Add portable Zed Gist sync configuration

This commit is contained in:
2026-07-29 17:11:18 +08:00
commit 13cf327f48
17 changed files with 1749 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
backups/
*.tmp
*.log
+34
View File
@@ -0,0 +1,34 @@
# Zed AI setup
## Codex in the Agent Panel
The live configuration now has `disable_ai: false` and already registers `codex-acp`. Restart or reload Zed, open the ACP Registry, install or confirm Codex, then create a Codex thread from the Agent Panel. Codex owns its authentication and billing; use its native login flow for ChatGPT or API-based authentication supported by the installed version.
This does not require a VS Code extension. Zed hosts Codex through ACP.
## Self-hosted or compatible LLM API
Use an OpenAI-compatible provider for an endpoint implementing OpenAI chat-completions or Responses semantics:
```jsonc
"language_models": {
"openai_compatible": {
"my-provider": {
"api_url": "https://llm.example.com/v1",
"available_models": [
{
"name": "my-model",
"display_name": "My Model",
"max_tokens": 128000
}
]
}
}
}
```
Enter the key through **Agent Settings > LLM Providers**, or set `MY_PROVIDER_API_KEY`. Do not put the key in `settings.json`.
Anthropic-compatible endpoints can be configured under `language_models.anthropic_compatible`. First-class providers include Anthropic, OpenAI, Google AI, Mistral, DeepSeek, xAI, and OpenCode. Zed also supports OpenRouter, Vercel AI Gateway, Bedrock, Ollama, LM Studio, llama.cpp, and local OpenAI-compatible servers.
Using your own key or local endpoint does not require paying for Zed-hosted models. The API provider may still charge for usage. Zed-hosted models use Zed's plan and usage billing.
+102
View File
@@ -0,0 +1,102 @@
# Zed portable configuration
This directory is the portable source for Zed settings, keybindings, tasks, extension installation preferences, and the file-exclusion toggle.
## 一行安装
前置依赖:已安装 Node.js 和 GitHub CLI `gh`
Windows PowerShell
```powershell
irm https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.ps1 | iex
```
macOS/Linux
```sh
curl -fsSL https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.sh | sh
```
脚本会处理 GitHub 登录、询问 Gist URL/ID、从 Gist 下载单个 `zed-settings-sync.json`、备份当前 Zed 配置并完成安装。安装后重启 Zed。
For first-machine setup, new-machine onboarding, daily Gist synchronization, and conflict handling, see [`SYNC-MANUAL.zh-CN.md`](./SYNC-MANUAL.zh-CN.md).
## Files
- `settings.json`: editor settings and extension installation list. Provider secrets are not stored here.
- `keymap.json`: cross-platform bindings using `secondary-` for Command/Ctrl.
- `tasks.json`: task template. Script-path placeholders are rendered during installation.
- `scripts/file-exclusions.json`: the only file to edit when changing permanent or toggleable exclusion rules.
- `scripts/toggle-file-scan-exclusions.mjs`: updates `file_scan_exclusions` in the active Zed settings.
- `scripts/settings-sync.json`: machine-local Gist ID and last synchronized hash. Export and import never overwrite it.
- `scripts/zed-settings-sync.mjs`: single-JSON import, export, hashing, and Gist synchronization.
- `bootstrap-from-gist.ps1` / `bootstrap-from-gist.sh`: standalone one-line installers for a fresh machine. They download the single JSON directly from Gist and do not need the rest of this directory.
## Zed Task Picker commands
Open the Task Picker and search for `Zed Settings`:
- `Export JSON...` and `Import JSON...` show a native file picker.
- `Configure GitHub Gist...` accepts a Gist ID or complete URL and remembers it on this machine.
- `Show GitHub Gist Status` compares the current, remote, and last synchronized SHA-256 hashes.
- `Push to GitHub Gist` and `Pull from GitHub Gist` use the remembered Gist and ask before overwriting divergent settings.
- `Authenticate GitHub...` runs `gh auth login` in the terminal.
GitHub CLI (`gh`) must be installed. The Gist must already exist and may contain other files; synchronization only creates or replaces `zed-settings-sync.json` inside it.
The synchronized JSON is deliberately flat. Its top-level keys are paths such as `settings.json`, `keymap.json`, and `scripts/file-exclusions.json`, whose values are the complete file contents. No version, metadata, `files` wrapper, or hash is embedded. SHA-256 values are calculated at runtime and only the last synchronized value is retained locally.
A secret Gist is unlisted, not access-controlled: anyone who obtains its URL can read it. This bundle deliberately excludes API keys, but a synchronized folder or private Git repository is the safer choice for configuration that contains sensitive paths or identifiers.
## Installed keybindings
- `Mod+R`: open the Task Picker; search for `Zed Settings` to import, export, push, or pull.
- `Mod+; U`: upload the current single JSON to the configured Gist.
- `Mod+; D`: download and import the single JSON from the configured Gist.
- `Mod+9` / `Mod+0`: activate the previous / next tab in the current pane.
- `Alt+1` through `Alt+5`: activate editor pane 1 through 5.
- `Mod+Shift+R`: switch to a recent project.
- `Mod+; K`: reload Zed.
- `Mod+; C`: focus the Git panel; press it again to focus the commit-message editor.
`Mod` is Command on macOS and Ctrl on Windows/Linux because the keymap uses Zed's `secondary-` syntax. `Alt` maps to Option on macOS.
## Windows
Install this copy into Zed:
```powershell
.\install.ps1
```
Export later changes from Zed back into this directory:
```powershell
.\export.ps1
```
## macOS and Linux
Node.js is required for the exclusion toggle and task-template rendering.
```sh
sh ./install.sh
sh ./export.sh
```
## Private Git synchronization
Initialize this directory once and push it to a private remote:
```sh
git init
git add .
git commit -m "Add portable Zed configuration"
git remote add origin <private-repository-url>
git push -u origin main
```
On another machine, clone the repository and run the platform installer. After changing Zed settings, run the export script, review `git diff`, then commit and push. On other machines, pull and run the installer again.
Zed account login does not currently synchronize these files. API keys remain in each machine's system keychain or environment variables and must not be committed.
+256
View File
@@ -0,0 +1,256 @@
# Zed 设置同步使用手册
本方案使用 GitHub Gist 中的单个 `zed-settings-sync.json` 同步 Zed 设置。JSON 顶层直接保存各配置文件的路径和完整内容;SHA-256 只在运行时计算,不写入同步文件。
## 1. 同步内容
会同步:
- `settings.json`:界面、字体、主题、编辑器行为及扩展自动安装清单。
- `keymap.json`:自定义键位。
- `tasks.json`:文件显隐和设置同步任务。
- `scripts/` 中的文件显隐及同步脚本。
- 非空的 `snippets/``themes/` 目录。
不会同步:
- GitHub 登录凭据、API Key 和系统环境变量。
- 本机保存的 Gist ID 和上次同步哈希。
- Zed 数据库、缓存、聊天记录、项目文件及扩展二进制。
扩展 ID 位于 `settings.json``auto_install_extensions` 中。导入后由 Zed 在目标平台重新安装扩展。
## 2. 准备 Gist
1. 登录 GitHub,打开 <https://gist.github.com/>。
2. 创建一个 Secret Gist。GitHub 不允许创建完全空白的 Gist,可以先放一个 `README.md`
3. 复制完整 Gist URL 或末尾的 Gist ID。
Gist ID 不是访问令牌。身份认证由 GitHub CLI `gh` 管理。
Secret Gist 只是不出现在公开搜索中,获得链接的人仍然可以读取其内容。同步 JSON 不包含 API Key,但仍应妥善保管 Gist URL。
## 3. 主机器首次上传
### 3.1 安装和认证 GitHub CLI
当前 Windows 机器已经安装 `gh`。其他机器可以使用系统包管理器安装:
```powershell
winget install GitHub.cli
```
macOS
```sh
brew install gh
```
安装后,在 Zed 中按 `Mod+R` 打开 Task Picker,运行:
```text
Zed Settings: Authenticate GitHub...
```
也可以在终端执行:
```sh
gh auth login
```
### 3.2 保存 Gist
1.`Mod+R`
2. 运行 `Zed Settings: Configure GitHub Gist...`
3. 粘贴 Gist URL 或 Gist ID。
4. 运行 `Zed Settings: Push to GitHub Gist`
5. 运行 `Zed Settings: Show GitHub Gist Status`,确认状态为 `Synchronized`
Push 只会在该 Gist 中创建或更新 `zed-settings-sync.json`,不会删除 Gist 中的其他文件。
Gist ID 和上次同步哈希保存在本机:
```text
Windows: %APPDATA%\Zed\scripts\settings-sync.json
macOS/Linux: ~/.config/zed/scripts/settings-sync.json
```
此文件不参与导入导出,所以其他机器不会覆盖当前机器记录。
## 4. 新机器首次接入
第一次接入只需要运行仓库里的独立 bootstrap 脚本。它会询问 Gist URL/ID,直接读取 Gist 单 JSON,再安装全部设置和同步工具;不需要下载或解压便携目录。
### 4.1 安装前置程序
新机器需要:
- Zed 或 ZedG。
- Node.js。
- GitHub CLI `gh`
bootstrap 仓库本身不保存设置或凭据,只需要包含:
```text
bootstrap-from-gist.ps1
bootstrap-from-gist.sh
```
因此这个仓库可以公开;真正的设置仍只存放在指定 Gist 中。
### 4.2 一行安装
Windows PowerShell
```powershell
irm https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.ps1 | iex
```
macOS/Linux
```sh
curl -fsSL https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.sh | sh
```
迁移到其他 Gitea 仓库时,Raw URL 的常见形式是:
```powershell
irm https://<Gitea域名>/<用户>/<仓库>/raw/branch/main/bootstrap-from-gist.ps1 | iex
```
```sh
curl -fsSL https://<Gitea域名>/<用户>/<仓库>/raw/branch/main/bootstrap-from-gist.sh | sh
```
执行过程中脚本会:
1. 检查 Node.js 和 `gh`
2. 在尚未登录时运行 `gh auth login`
3. 询问 Gist URL 或 ID。
4. 从 GitHub API 读取 `zed-settings-sync.json`
5. 验证顶层文件路径,防止写出 Zed 配置目录。
6. 备份新机器已有配置。
7. 安装设置、键位、Tasks、snippets、themes 和辅助脚本。
8. 保存 Gist ID 和远端整文件 SHA-256 作为首次同步基线。
安装完成后重启 Zed,运行 `Zed Settings: Show GitHub Gist Status`,状态应为 `Synchronized`
为了降低执行远程脚本的供应链风险,可以把 URL 中的 `main` 换成审核过的 commit SHA。bootstrap 仓库不应加入访问令牌或其他秘密。
### 4.3 便携目录备用安装
不能访问 Raw URL 时,仍可复制完整 `zed-config-portable` 目录并运行:
```powershell
.\install.ps1
```
```sh
sh ./install.sh
```
然后在 Task Picker 中认证 GitHub、配置 Gist 并执行首次 Pull。
## 5. 之后的日常使用
开始在一台机器工作前:
1.`Mod+R`
2. 运行 `Zed Settings: Show GitHub Gist Status`
3. 如果显示 `Remote changes available`,执行 Pull。
修改设置、键位、Tasks、排除规则或 snippets 后:
1. 执行 Push。
2. 确认状态变为 `Synchronized`
3. 再到其他机器执行 Pull。
常用快捷键:
| 快捷键 | 操作 |
|---|---|
| `Mod+R` | 打开 Task Picker |
| `Mod+; U` | Push 到 Gist |
| `Mod+; D` | 从 Gist Pull |
| `Mod+; K` | Reload Zed |
`Mod` 在 Windows/Linux 是 Ctrl,在 macOS 是 Command。
## 6. 哈希和冲突处理
系统比较三个 SHA-256
- 当前机器实时生成的单 JSON 哈希。
- Gist 中单 JSON 的哈希。
- 当前机器上次成功 Push/Pull 时保存的哈希。
Status 可能显示:
| 状态 | 含义 | 建议操作 |
|---|---|---|
| `Synchronized` | 本地和 Gist 一致 | 无需操作 |
| `Local changes pending upload` | 只有本机有变化 | Push |
| `Remote changes available` | 只有 Gist 有变化 | Pull |
| `Conflict` | 本机和 Gist 都有变化 | 先导出本地备份,再决定 Push 或 Pull |
| `Not synchronized on this machine` | 这台机器尚无同步基线 | 首次接入通常选择 Pull |
Push 覆盖远端前、Pull 覆盖本地前都会显示哈希并要求确认。Pull 会自动备份本地配置;系统不会尝试逐字段自动合并。
## 7. 离线 JSON 导入导出
不使用 Gist 时,也可以使用完全相同的单 JSON 格式:
```text
Zed Settings: Export JSON...
Zed Settings: Import JSON...
```
两个命令都会弹出系统文件选择窗口。直接 Import 不会修改已保存的 Gist ID 和上次同步哈希;导入内容会被视为新的本地变化,可以随后 Push 到 Gist。
## 8. 修改文件排除规则
只编辑本机配置中的 `custom` 数组:
```text
Windows: %APPDATA%\Zed\scripts\file-exclusions.json
macOS/Linux: ~/.config/zed/scripts/file-exclusions.json
```
修改后按 `Alt+Shift+-` 测试显隐,再 Push 到 Gist。其他机器 Pull 后会取得相同规则。
## 9. 备份位置
每次 Import 或 Pull 前的备份位于:
```text
Windows: %APPDATA%\Zed\backups\sync-import-<时间>
macOS/Linux: ~/.config/zed/backups/sync-import-<时间>
```
便携安装器产生的备份使用 `portable-install-<时间>` 名称。
## 10. 常见问题
### Task Picker 中没有 Zed Settings
重新运行一行 bootstrap 命令,或运行便携目录中的 `install.ps1` / `install.sh`,然后 Reload Zed。
### 提示找不到 gh
安装 GitHub CLI 后完全退出并重启 Zed,使编辑器读取更新后的 PATH。
### Gist 返回 404
检查 Gist URL/ID、当前 GitHub 账号以及 Gist 是否仍然存在。重新运行 `Configure GitHub Gist...` 可以更换记录。
### Gist 中没有 zed-settings-sync.json
先在已经配置好的主机器上执行一次 Push。
### Pull 后键位或界面没有立即变化
执行 `Mod+; K` Reload Zed;如果扩展仍在安装,等待完成后再重启一次。
### 换了另一个 Gist
运行 `Configure GitHub Gist...` 并输入新 ID。系统会保留新 ID,同时清空旧 Gist 对应的上次同步哈希,下一次 Push/Pull 会重新确认覆盖方向。
+134
View File
@@ -0,0 +1,134 @@
[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$bundleName = "zed-settings-sync.json"
$utf8 = New-Object System.Text.UTF8Encoding($false)
function Require-Command([string]$Name, [string]$InstallHint) {
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name is required. Install it first: $InstallHint"
}
}
function Get-GistId([string]$Value) {
$matches = [regex]::Matches($Value.Trim(), "[a-fA-F0-9]{8,}")
if ($matches.Count -eq 0) {
throw "Enter a valid GitHub Gist ID or URL."
}
return $matches[$matches.Count - 1].Value.ToLowerInvariant()
}
function Test-AllowedPath([string]$Path) {
$fixed = @(
"settings.json",
"keymap.json",
"tasks.json",
"debug.json",
"scripts/toggle-file-scan-exclusions.mjs",
"scripts/file-exclusions.json",
"scripts/zed-settings-sync.mjs"
)
if ($Path.StartsWith("/") -or $Path.Contains("\") -or $Path.Split("/") -contains "..") {
return $false
}
return $fixed -contains $Path -or $Path.StartsWith("snippets/") -or $Path.StartsWith("themes/")
}
function Write-Atomic([string]$Path, [string]$Contents) {
$directory = Split-Path $Path -Parent
New-Item -ItemType Directory -Force -Path $directory | Out-Null
$temporary = Join-Path $directory ".$([IO.Path]::GetFileName($Path)).$PID.tmp"
try {
[IO.File]::WriteAllText($temporary, $Contents, $utf8)
Move-Item -LiteralPath $temporary -Destination $Path -Force
} finally {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
}
}
Require-Command "node" "winget install OpenJS.NodeJS.LTS"
Require-Command "gh" "winget install GitHub.cli"
& gh auth status *> $null
if ($LASTEXITCODE -ne 0) {
Write-Host "GitHub authentication is required."
& gh auth login
if ($LASTEXITCODE -ne 0) { throw "GitHub authentication failed." }
}
$entered = if ($env:ZED_SETTINGS_GIST_ID) { $env:ZED_SETTINGS_GIST_ID } else { Read-Host "GitHub Gist ID or full URL" }
$gistId = Get-GistId $entered
if ($env:ZED_SETTINGS_BUNDLE_PATH) {
$bundleSource = [IO.File]::ReadAllText($env:ZED_SETTINGS_BUNDLE_PATH, [Text.Encoding]::UTF8)
} else {
$token = (& gh auth token).Trim()
if ($LASTEXITCODE -ne 0 -or -not $token) { throw "Could not obtain the GitHub authentication token from gh." }
$headers = @{
Accept = "application/vnd.github+json"
Authorization = "Bearer $token"
"User-Agent" = "zed-settings-bootstrap"
"X-GitHub-Api-Version" = "2022-11-28"
}
$gist = Invoke-RestMethod -Uri "https://api.github.com/gists/$gistId" -Headers $headers
$remoteFile = $gist.files.$bundleName
if (-not $remoteFile) { throw "Gist $gistId does not contain $bundleName." }
if ($remoteFile.truncated) { throw "$bundleName is too large for the Gist API response." }
$bundleSource = [string]$remoteFile.content
}
try {
$bundle = $bundleSource | ConvertFrom-Json
} catch {
throw "The Gist contains invalid JSON: $($_.Exception.Message)"
}
$properties = @($bundle.PSObject.Properties)
foreach ($required in @("settings.json", "keymap.json", "tasks.json")) {
if (-not ($properties.Name -contains $required)) { throw "The bundle is missing $required." }
}
foreach ($property in $properties) {
if (-not (Test-AllowedPath $property.Name) -or $property.Value -isnot [string]) {
throw "Invalid bundle file: $($property.Name)"
}
}
$target = Join-Path $env:APPDATA "Zed"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backup = Join-Path $target "backups\gist-bootstrap-$timestamp"
New-Item -ItemType Directory -Force -Path $target, $backup, (Join-Path $target "scripts") | Out-Null
foreach ($property in $properties) {
$existing = Join-Path $target ($property.Name.Replace("/", "\"))
if (Test-Path -LiteralPath $existing) {
$backupPath = Join-Path $backup ($property.Name.Replace("/", "\"))
New-Item -ItemType Directory -Force -Path (Split-Path $backupPath -Parent) | Out-Null
Copy-Item -LiteralPath $existing -Destination $backupPath -Force
}
}
$toggleScript = (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs").Replace("\", "/")
$syncScript = (Join-Path $target "scripts\zed-settings-sync.mjs").Replace("\", "/")
foreach ($property in $properties) {
$destination = Join-Path $target ($property.Name.Replace("/", "\"))
$contents = [string]$property.Value
if ($property.Name -eq "tasks.json") {
$contents = $contents.Replace("__TOGGLE_SCRIPT__", $toggleScript).Replace("__SYNC_SCRIPT__", $syncScript)
}
Write-Atomic $destination $contents
}
$bytes = $utf8.GetBytes($bundleSource)
$sha = [Security.Cryptography.SHA256]::Create()
try {
$hash = -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
} finally {
$sha.Dispose()
}
$syncState = [ordered]@{ gist_id = $gistId; last_synced_hash = $hash } | ConvertTo-Json
Write-Atomic (Join-Path $target "scripts\settings-sync.json") "$syncState`n"
Write-Host "Installed Zed settings from Gist $gistId"
Write-Host "SHA-256: $hash"
Write-Host "Backup: $backup"
Write-Host "Restart Zed to load the synchronized configuration."
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env sh
set -eu
command -v node >/dev/null 2>&1 || {
printf '%s\n' 'Node.js is required. Install it with your system package manager.' >&2
exit 1
}
command -v gh >/dev/null 2>&1 || {
printf '%s\n' 'GitHub CLI (gh) is required: https://cli.github.com/' >&2
exit 1
}
if ! gh auth status >/dev/null 2>&1; then
printf '%s\n' 'GitHub authentication is required.'
gh auth login </dev/tty >/dev/tty
fi
if [ -n "${ZED_SETTINGS_GIST_ID:-}" ]; then
entered=$ZED_SETTINGS_GIST_ID
else
printf 'GitHub Gist ID or full URL: ' >/dev/tty
IFS= read -r entered </dev/tty
fi
gist_id=$(printf '%s' "$entered" | sed -nE 's#.*[^a-fA-F0-9]([a-fA-F0-9]{8,})/?$#\1#p')
if [ -z "$gist_id" ]; then
gist_id=$(printf '%s' "$entered" | sed -nE 's#^([a-fA-F0-9]{8,})$#\1#p')
fi
if [ -z "$gist_id" ]; then
printf '%s\n' 'Enter a valid GitHub Gist ID or URL.' >&2
exit 1
fi
temporary_dir=$(mktemp -d)
trap 'rm -rf "$temporary_dir"' EXIT INT TERM
response="$temporary_dir/gist.json"
if [ -z "${ZED_SETTINGS_BUNDLE_PATH:-}" ]; then
gh api "gists/$gist_id" > "$response"
fi
node - "$response" "$gist_id" "${ZED_SETTINGS_BUNDLE_PATH:-}" <<'NODE'
const { createHash } = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const [responsePath, gistId, localBundlePath] = process.argv.slice(2);
const bundleName = "zed-settings-sync.json";
let bundleSource;
if (localBundlePath) {
bundleSource = fs.readFileSync(localBundlePath, "utf8");
} else {
const gist = JSON.parse(fs.readFileSync(responsePath, "utf8"));
const remoteFile = gist.files?.[bundleName];
if (!remoteFile) throw new Error(`Gist ${gistId} does not contain ${bundleName}.`);
if (remoteFile.truncated) throw new Error(`${bundleName} is too large for the Gist API response.`);
bundleSource = remoteFile.content;
}
const bundle = JSON.parse(bundleSource);
const fixed = new Set([
"settings.json",
"keymap.json",
"tasks.json",
"debug.json",
"scripts/toggle-file-scan-exclusions.mjs",
"scripts/file-exclusions.json",
"scripts/zed-settings-sync.mjs",
]);
function allowed(relativePath) {
const parts = relativePath.split("/");
return typeof relativePath === "string"
&& !relativePath.startsWith("/")
&& !relativePath.includes("\\")
&& !parts.some((part) => !part || part === "." || part === "..")
&& (fixed.has(relativePath) || relativePath.startsWith("snippets/") || relativePath.startsWith("themes/"));
}
for (const required of ["settings.json", "keymap.json", "tasks.json"]) {
if (typeof bundle[required] !== "string") throw new Error(`The bundle is missing ${required}.`);
}
for (const [relativePath, contents] of Object.entries(bundle)) {
if (!allowed(relativePath) || typeof contents !== "string") throw new Error(`Invalid bundle file: ${relativePath}`);
}
const target = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "zed");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const backup = path.join(target, "backups", `gist-bootstrap-${stamp}`);
fs.mkdirSync(backup, { recursive: true });
for (const relativePath of Object.keys(bundle)) {
const existing = path.join(target, ...relativePath.split("/"));
if (!fs.existsSync(existing)) continue;
const backupPath = path.join(backup, ...relativePath.split("/"));
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
fs.copyFileSync(existing, backupPath);
}
const toggleScript = path.join(target, "scripts", "toggle-file-scan-exclusions.mjs").replaceAll("\\", "/");
const syncScript = path.join(target, "scripts", "zed-settings-sync.mjs").replaceAll("\\", "/");
for (const [relativePath, sourceContents] of Object.entries(bundle)) {
const destination = path.join(target, ...relativePath.split("/"));
let contents = sourceContents;
if (relativePath === "tasks.json") {
contents = contents.replaceAll("__TOGGLE_SCRIPT__", toggleScript).replaceAll("__SYNC_SCRIPT__", syncScript);
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}.tmp`);
fs.writeFileSync(temporary, contents, "utf8");
fs.renameSync(temporary, destination);
}
const hash = createHash("sha256").update(bundleSource).digest("hex");
const syncStatePath = path.join(target, "scripts", "settings-sync.json");
fs.mkdirSync(path.dirname(syncStatePath), { recursive: true });
fs.writeFileSync(syncStatePath, `${JSON.stringify({ gist_id: gistId.toLowerCase(), last_synced_hash: hash }, null, 2)}\n`, "utf8");
console.log(`Installed Zed settings from Gist ${gistId}`);
console.log(`SHA-256: ${hash}`);
console.log(`Backup: ${backup}`);
console.log("Restart Zed to load the synchronized configuration.");
NODE
+25
View File
@@ -0,0 +1,25 @@
[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$source = Join-Path $env:APPDATA "Zed"
$target = $PSScriptRoot
$utf8 = New-Object System.Text.UTF8Encoding($false)
Copy-Item -LiteralPath (Join-Path $source "settings.json") -Destination (Join-Path $target "settings.json") -Force
Copy-Item -LiteralPath (Join-Path $source "keymap.json") -Destination (Join-Path $target "keymap.json") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs") -Destination (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\file-exclusions.json") -Destination (Join-Path $target "scripts\file-exclusions.json") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\zed-settings-sync.mjs") -Destination (Join-Path $target "scripts\zed-settings-sync.mjs") -Force
$tasks = [IO.File]::ReadAllText((Join-Path $source "tasks.json"))
foreach ($script in @(
@{ Path = (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs"); Placeholder = "__TOGGLE_SCRIPT__" },
@{ Path = (Join-Path $source "scripts\zed-settings-sync.mjs"); Placeholder = "__SYNC_SCRIPT__" }
)) {
$tasks = $tasks.Replace($script.Path.Replace("\", "\\"), $script.Placeholder)
$tasks = $tasks.Replace($script.Path.Replace("\", "/"), $script.Placeholder)
}
[IO.File]::WriteAllText((Join-Path $target "tasks.json"), $tasks, $utf8)
Write-Host "Exported Zed configuration from $source"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env sh
set -eu
target=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source="${XDG_CONFIG_HOME:-$HOME/.config}/zed"
cp "$source/settings.json" "$target/settings.json"
cp "$source/keymap.json" "$target/keymap.json"
cp "$source/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/toggle-file-scan-exclusions.mjs"
cp "$source/scripts/file-exclusions.json" "$target/scripts/file-exclusions.json"
cp "$source/scripts/zed-settings-sync.mjs" "$target/scripts/zed-settings-sync.mjs"
node - "$source/tasks.json" "$target/tasks.json" "$source/scripts/toggle-file-scan-exclusions.mjs" "$source/scripts/zed-settings-sync.mjs" <<'NODE'
const fs = require("node:fs");
const [source, target, toggleScript, syncScript] = process.argv.slice(2);
const tasks = fs.readFileSync(source, "utf8")
.replaceAll(toggleScript, "__TOGGLE_SCRIPT__")
.replaceAll(syncScript, "__SYNC_SCRIPT__");
fs.writeFileSync(target, tasks);
NODE
printf 'Exported Zed configuration from %s\n' "$source"
+38
View File
@@ -0,0 +1,38 @@
[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$source = $PSScriptRoot
$target = Join-Path $env:APPDATA "Zed"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backup = Join-Path $target "backups\portable-install-$timestamp"
$utf8 = New-Object System.Text.UTF8Encoding($false)
New-Item -ItemType Directory -Force -Path $target, $backup, (Join-Path $target "scripts") | Out-Null
foreach ($name in @("settings.json", "keymap.json", "tasks.json")) {
$existing = Join-Path $target $name
if (Test-Path -LiteralPath $existing) {
Copy-Item -LiteralPath $existing -Destination (Join-Path $backup $name)
}
}
Copy-Item -LiteralPath (Join-Path $source "settings.json") -Destination (Join-Path $target "settings.json") -Force
Copy-Item -LiteralPath (Join-Path $source "keymap.json") -Destination (Join-Path $target "keymap.json") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs") -Destination (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\file-exclusions.json") -Destination (Join-Path $target "scripts\file-exclusions.json") -Force
Copy-Item -LiteralPath (Join-Path $source "scripts\zed-settings-sync.mjs") -Destination (Join-Path $target "scripts\zed-settings-sync.mjs") -Force
$syncConfig = Join-Path $target "scripts\settings-sync.json"
if (-not (Test-Path -LiteralPath $syncConfig)) {
Copy-Item -LiteralPath (Join-Path $source "scripts\settings-sync.json") -Destination $syncConfig
}
$toggleScriptPath = (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs").Replace("\", "/")
$syncScriptPath = (Join-Path $target "scripts\zed-settings-sync.mjs").Replace("\", "/")
$tasks = [IO.File]::ReadAllText((Join-Path $source "tasks.json"))
$tasks = $tasks.Replace("__TOGGLE_SCRIPT__", $toggleScriptPath)
$tasks = $tasks.Replace("__SYNC_SCRIPT__", $syncScriptPath)
[IO.File]::WriteAllText((Join-Path $target "tasks.json"), $tasks, $utf8)
Write-Host "Installed Zed configuration to $target"
Write-Host "Previous configuration backed up to $backup"
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env sh
set -eu
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
target="${XDG_CONFIG_HOME:-$HOME/.config}/zed"
timestamp=$(date +%Y%m%d-%H%M%S)
backup="$target/backups/portable-install-$timestamp"
mkdir -p "$target/scripts" "$backup"
for name in settings.json keymap.json tasks.json; do
if [ -f "$target/$name" ]; then
cp "$target/$name" "$backup/$name"
fi
done
cp "$source_dir/settings.json" "$target/settings.json"
cp "$source_dir/keymap.json" "$target/keymap.json"
cp "$source_dir/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/toggle-file-scan-exclusions.mjs"
cp "$source_dir/scripts/file-exclusions.json" "$target/scripts/file-exclusions.json"
cp "$source_dir/scripts/zed-settings-sync.mjs" "$target/scripts/zed-settings-sync.mjs"
if [ ! -f "$target/scripts/settings-sync.json" ]; then
cp "$source_dir/scripts/settings-sync.json" "$target/scripts/settings-sync.json"
fi
node - "$source_dir/tasks.json" "$target/tasks.json" "$target/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/zed-settings-sync.mjs" <<'NODE'
const fs = require("node:fs");
const [source, target, toggleScript, syncScript] = process.argv.slice(2);
const tasks = fs.readFileSync(source, "utf8")
.replaceAll("__TOGGLE_SCRIPT__", toggleScript)
.replaceAll("__SYNC_SCRIPT__", syncScript);
fs.writeFileSync(target, tasks);
NODE
printf 'Installed Zed configuration to %s\n' "$target"
printf 'Previous configuration backed up to %s\n' "$backup"
+81
View File
@@ -0,0 +1,81 @@
[
{
"bindings": {
"f12": ["zed::IncreaseUiFontSize", { "persist": false }],
"secondary-f12": ["zed::DecreaseUiFontSize", { "persist": false }],
"secondary-1": "project_panel::ToggleFocus",
"secondary-2": "terminal_panel::ToggleFocus",
"secondary-3": "workspace::ToggleRightDock",
"secondary-shift-g": "git_panel::ToggleFocus",
"secondary-shift-v": "zed::Extensions",
"secondary-p": "command_palette::Toggle",
"secondary-o": "file_finder::Toggle",
"secondary-9": "pane::ActivatePreviousItem",
"secondary-0": "pane::ActivateNextItem",
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
"alt-3": ["workspace::ActivatePane", 2],
"alt-4": ["workspace::ActivatePane", 3],
"alt-5": ["workspace::ActivatePane", 4],
"secondary-shift-o": "workspace::Open",
"secondary-enter": "workspace::ToggleZoom",
"secondary-r": "task::Spawn",
"secondary-shift-r": "projects::OpenRecent",
"shift-alt-r": null,
"secondary-; k": "workspace::Reload",
"secondary-; g": "git::FileHistory",
"secondary-; p": "git::Push",
"secondary-; secondary-p": "git::Pull",
"secondary-; n": "git::Branch",
"secondary-; s": "git::StashAll",
"secondary-; z": "git::StashPop",
"secondary-; a": "git::StageAll",
"secondary-; u": [
"task::Spawn",
{ "task_name": "Zed Settings: Push to GitHub Gist" }
],
"secondary-; d": [
"task::Spawn",
{ "task_name": "Zed Settings: Pull from GitHub Gist" }
],
"shift-alt--": [
"task::Spawn",
{ "task_name": "Toggle excluded files" }
]
}
},
{
"context": "Workspace",
"bindings": {
"secondary-; c": "git_panel::ToggleFocus"
}
},
{
"context": "GitPanel",
"bindings": {
"secondary-; c": "git_panel::FocusEditor"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"secondary-shift-[": "editor::Fold",
"secondary-shift-]": "editor::UnfoldLines",
"secondary--": "editor::FoldAll",
"secondary-=": "editor::UnfoldAll"
}
},
{
"context": "Editor && extension == md",
"bindings": {
"alt-p": "markdown::OpenPreview",
"shift-alt-p": "markdown::OpenPreviewToTheSide"
}
},
{
"context": "ProjectPanel",
"bindings": {
"alt--": "project_panel::CollapseAllEntries"
}
}
]
+59
View File
@@ -0,0 +1,59 @@
{
"defaults": [
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
"**/.classpath",
"**/.settings"
],
"custom": [
"**/.playwright-cli",
"**/output",
"**/.cursor",
"**/.vscode",
"**/.idea",
"**/.fleet",
"**/node_modules",
"**/bun.lockb",
"**/pnpm-lock.yaml",
"**/package-lock.json",
"**/yarn.lock",
"**/dist",
"**/out",
"**/build",
"**/.next",
"**/.nuxt",
"**/.output",
"**/.cache",
"**/.turbo",
"**/.vercel",
"**/.netlify",
"**/tsconfig.tsbuildinfo",
"**/auto-imports.d.ts",
"**/components.json",
"**/unplugin-vue-components.d.ts",
"src/*.d.ts",
"**/__pycache__",
"**/__init__.py",
"**/.ipynb_checkpoints",
"**/.pytest_cache",
"**/.mypy_cache",
"**/.venv",
"**/env",
"**/venv",
"**/.github",
"**/.husky",
"**/.npmrc",
"**/.eslintrc*",
"**/.prettier*",
"**/.editorconfig",
"**/.dockerignore",
"**/docker-compose.override.yml",
"**/cypress*",
"**/prisma/migrations"
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"gist_id": "",
"last_synced_hash": ""
}
+88
View File
@@ -0,0 +1,88 @@
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { readFile, rename, rm, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
async function exclusionRules() {
const rulesPath = join(scriptDirectory, "file-exclusions.json");
const rules = JSON.parse(await readFile(rulesPath, "utf8"));
if (!Array.isArray(rules.defaults) || !Array.isArray(rules.custom)) {
throw new Error("file-exclusions.json must contain defaults and custom arrays");
}
return {
defaults: [...new Set(rules.defaults)],
custom: [...new Set(rules.custom)].filter(
(entry) => !rules.defaults.includes(entry),
),
};
}
function settingsPath() {
if (process.platform === "win32") {
const appData = process.env.APPDATA;
if (!appData) throw new Error("APPDATA is not set");
return join(appData, "Zed", "settings.json");
}
return join(homedir(), ".config", "zed", "settings.json");
}
function findArrayRange(source) {
const property = /"file_scan_exclusions"\s*:/g.exec(source);
if (!property) throw new Error("file_scan_exclusions is missing from settings.json");
const start = source.indexOf("[", property.index + property[0].length);
if (start === -1) throw new Error("file_scan_exclusions is not an array");
let inString = false;
let escaped = false;
let depth = 0;
for (let index = start; index < source.length; index += 1) {
const character = source[index];
if (inString) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === '"') inString = false;
continue;
}
if (character === '"') inString = true;
else if (character === "[") depth += 1;
else if (character === "]" && --depth === 0) return { start, end: index + 1 };
}
throw new Error("file_scan_exclusions array is not closed");
}
function formatArray(values, indent, newline) {
const itemIndent = `${indent} `;
const items = values.map((value) => `${itemIndent}${JSON.stringify(value)}`);
return `[${newline}${items.join(`,${newline}`)}${newline}${indent}]`;
}
async function replaceAtomically(path, contents) {
const temporaryPath = join(dirname(path), `.settings.json.${process.pid}.tmp`);
try {
await writeFile(temporaryPath, contents, "utf8");
await rename(temporaryPath, path);
} finally {
await rm(temporaryPath, { force: true });
}
}
const path = settingsPath();
const { defaults, custom } = await exclusionRules();
const source = await readFile(path, "utf8");
const newline = source.includes("\r\n") ? "\r\n" : "\n";
const range = findArrayRange(source);
const current = JSON.parse(source.slice(range.start, range.end));
const customIsActive = current.some((entry) => !defaults.includes(entry));
const next = customIsActive ? defaults : [...defaults, ...custom];
const lineStart = source.lastIndexOf("\n", range.start) + 1;
const indent = source.slice(lineStart, range.start).match(/^\s*/)?.[0] ?? "";
const updated =
source.slice(0, range.start) +
formatArray(next, indent, newline) +
source.slice(range.end);
await replaceAtomically(path, updated);
console.log(`Excluded files are now ${customIsActive ? "shown" : "hidden"}.`);
+440
View File
@@ -0,0 +1,440 @@
import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
const BUNDLE_NAME = "zed-settings-sync.json";
const REQUIRED_FILES = ["settings.json", "keymap.json", "tasks.json"];
const FIXED_FILES = [
...REQUIRED_FILES,
"debug.json",
"scripts/toggle-file-scan-exclusions.mjs",
"scripts/file-exclusions.json",
"scripts/zed-settings-sync.mjs",
];
const OPTIONAL_DIRECTORIES = ["snippets", "themes"];
const LOCAL_SYNC_CONFIG = "scripts/settings-sync.json";
function zedConfigDirectory() {
if (process.platform === "win32") {
if (!process.env.APPDATA) throw new Error("APPDATA is not set");
return join(process.env.APPDATA, "Zed");
}
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "zed");
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
async function writeAtomically(path, contents) {
await mkdir(dirname(path), { recursive: true });
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
try {
await writeFile(temporaryPath, contents, "utf8");
await rename(temporaryPath, path);
} finally {
await rm(temporaryPath, { force: true });
}
}
async function listFilesRecursively(directory, prefix) {
if (!existsSync(directory)) return [];
const results = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const absolutePath = join(directory, entry.name);
const relativePath = `${prefix}/${entry.name}`;
if (entry.isDirectory()) results.push(...await listFilesRecursively(absolutePath, relativePath));
else if (entry.isFile()) results.push(relativePath);
}
return results;
}
async function synchronizedPaths(configDirectory) {
const paths = FIXED_FILES.filter((path) => existsSync(join(configDirectory, ...path.split("/"))));
for (const directory of OPTIONAL_DIRECTORIES) {
paths.push(...await listFilesRecursively(join(configDirectory, directory), directory));
}
return [...new Set(paths)].sort();
}
function isAllowedBundlePath(path) {
if (typeof path !== "string" || path.startsWith("/") || path.includes("\\")) return false;
const segments = path.split("/");
if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
if (FIXED_FILES.includes(path) || path === LOCAL_SYNC_CONFIG) return true;
return OPTIONAL_DIRECTORIES.some((directory) => path.startsWith(`${directory}/`));
}
function replaceTaskPath(source, path, placeholder) {
return source
.replaceAll(path.replaceAll("\\", "\\\\"), placeholder)
.replaceAll(path.replaceAll("\\", "/"), placeholder);
}
function portableTasks(source, configDirectory) {
return replaceTaskPath(
replaceTaskPath(
source,
join(configDirectory, "scripts", "toggle-file-scan-exclusions.mjs"),
"__TOGGLE_SCRIPT__",
),
join(configDirectory, "scripts", "zed-settings-sync.mjs"),
"__SYNC_SCRIPT__",
);
}
function installedTasks(source, configDirectory) {
return source
.replaceAll(
"__TOGGLE_SCRIPT__",
join(configDirectory, "scripts", "toggle-file-scan-exclusions.mjs").replaceAll("\\", "/"),
)
.replaceAll(
"__SYNC_SCRIPT__",
join(configDirectory, "scripts", "zed-settings-sync.mjs").replaceAll("\\", "/"),
);
}
function hashContents(contents) {
return createHash("sha256").update(contents).digest("hex");
}
async function createBundle() {
const configDirectory = zedConfigDirectory();
const files = {};
for (const relativePath of await synchronizedPaths(configDirectory)) {
const absolutePath = join(configDirectory, ...relativePath.split("/"));
let contents = await readFile(absolutePath, "utf8");
if (relativePath === "tasks.json") contents = portableTasks(contents, configDirectory);
files[relativePath] = contents;
}
for (const required of REQUIRED_FILES) {
if (!(required in files)) throw new Error(`Cannot export: ${required} is missing`);
}
const contents = serializeBundle(files);
return { files, contents, hash: hashContents(contents) };
}
function serializeBundle(files) {
return `${JSON.stringify(files, null, 2)}\n`;
}
function parseBundle(contents, sourceName) {
let files;
try {
files = JSON.parse(contents);
} catch (error) {
throw new Error(`Invalid JSON in ${sourceName}: ${error.message}`);
}
if (!files || Array.isArray(files) || typeof files !== "object") {
throw new Error(`Invalid Zed settings bundle: ${sourceName}`);
}
for (const required of REQUIRED_FILES) {
if (typeof files[required] !== "string") {
throw new Error(`Invalid bundle: ${required} is missing`);
}
}
for (const [path, value] of Object.entries(files)) {
if (!isAllowedBundlePath(path) || typeof value !== "string") {
throw new Error(`Invalid bundle file: ${path}`);
}
}
return { files, hash: hashContents(contents) };
}
async function exportBundle(path) {
const bundle = await createBundle();
await writeAtomically(path, bundle.contents);
console.log(`Exported Zed settings to ${path}`);
console.log(`SHA-256: ${bundle.hash}`);
return bundle;
}
async function backupCurrentConfiguration(configDirectory) {
const backupDirectory = join(configDirectory, "backups", `sync-import-${timestamp()}`);
await mkdir(backupDirectory, { recursive: true });
for (const relativePath of await synchronizedPaths(configDirectory)) {
const source = join(configDirectory, ...relativePath.split("/"));
const destination = join(backupDirectory, ...relativePath.split("/"));
await mkdir(dirname(destination), { recursive: true });
await copyFile(source, destination);
}
return backupDirectory;
}
async function installBundleContents(contents, sourceName) {
const parsed = parseBundle(contents, sourceName);
const configDirectory = zedConfigDirectory();
const backupDirectory = await backupCurrentConfiguration(configDirectory);
for (const [relativePath, sourceContents] of Object.entries(parsed.files)) {
// Gist identity and sync history belong to each machine and are never imported.
if (relativePath === LOCAL_SYNC_CONFIG) continue;
const destination = join(configDirectory, ...relativePath.split("/"));
const installedContents = relativePath === "tasks.json"
? installedTasks(sourceContents, configDirectory)
: sourceContents;
await writeAtomically(destination, installedContents);
}
console.log(`Imported Zed settings from ${sourceName}`);
console.log(`Previous settings were backed up to ${backupDirectory}`);
return { ...parsed, backupDirectory };
}
async function importBundle(path) {
return installBundleContents(await readFile(path, "utf8"), path);
}
function runPowerShell(command, extraEnv = {}) {
const result = spawnSync("powershell.exe", ["-NoProfile", "-STA", "-Command", command], {
encoding: "utf8",
env: { ...process.env, ...extraEnv },
windowsHide: true,
});
if (result.status !== 0) throw new Error(result.stderr.trim() || "Windows dialog failed");
return result.stdout.trim();
}
function nativeFileDialog(mode) {
if (process.platform === "win32") {
const common = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; ";
const command = mode === "save"
? `${common}$d=New-Object System.Windows.Forms.SaveFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; $d.AddExtension=$true; if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}`
: `${common}$d=New-Object System.Windows.Forms.OpenFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}`;
return runPowerShell(command);
}
if (process.platform === "darwin") {
const script = mode === "save"
? `POSIX path of (choose file name with prompt "Export Zed settings" default name "${BUNDLE_NAME}")`
: "POSIX path of (choose file with prompt \"Import Zed settings\")";
const result = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
return result.status === 0 ? result.stdout.trim() : "";
}
const args = ["--file-selection", `--title=${mode === "save" ? "Export" : "Import"} Zed settings`];
if (mode === "save") args.push("--save", "--confirm-overwrite", `--filename=${BUNDLE_NAME}`);
const result = spawnSync("zenity", args, { encoding: "utf8" });
return result.status === 0 ? result.stdout.trim() : "";
}
function nativeInput(prompt, title, defaultValue = "") {
if (process.platform === "win32") {
const command = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName Microsoft.VisualBasic; [Console]::Write([Microsoft.VisualBasic.Interaction]::InputBox($env:ZED_SYNC_PROMPT,$env:ZED_SYNC_TITLE,$env:ZED_SYNC_DEFAULT))";
return runPowerShell(command, {
ZED_SYNC_PROMPT: prompt,
ZED_SYNC_TITLE: title,
ZED_SYNC_DEFAULT: defaultValue,
});
}
if (process.platform === "darwin") {
const escaped = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
const script = `text returned of (display dialog "${escaped(prompt)}" with title "${escaped(title)}" default answer "${escaped(defaultValue)}")`;
const result = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
return result.status === 0 ? result.stdout.trim() : "";
}
const result = spawnSync("zenity", ["--entry", `--title=${title}`, `--text=${prompt}`, `--entry-text=${defaultValue}`], { encoding: "utf8" });
return result.status === 0 ? result.stdout.trim() : "";
}
function nativeMessage(message, title = "Zed Settings Sync", error = false) {
if (process.platform === "win32") {
const command = "Add-Type -AssemblyName System.Windows.Forms; [void][System.Windows.Forms.MessageBox]::Show($env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'OK',$env:ZED_SYNC_ICON)";
runPowerShell(command, {
ZED_SYNC_MESSAGE: message,
ZED_SYNC_TITLE: title,
ZED_SYNC_ICON: error ? "Error" : "Information",
});
return;
}
if (process.platform === "darwin") {
const escaped = message.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
spawnSync("osascript", ["-e", `display dialog "${escaped}" with title "${title}" buttons {"OK"}`]);
return;
}
spawnSync("zenity", [error ? "--error" : "--info", `--title=${title}`, `--text=${message}`]);
}
function nativeConfirm(message, title = "Zed Settings Sync") {
if (process.platform === "win32") {
const command = "Add-Type -AssemblyName System.Windows.Forms; [Console]::Write([System.Windows.Forms.MessageBox]::Show($env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'YesNo','Warning'))";
return runPowerShell(command, { ZED_SYNC_MESSAGE: message, ZED_SYNC_TITLE: title }) === "Yes";
}
if (process.platform === "darwin") {
const escaped = message.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
const result = spawnSync("osascript", ["-e", `button returned of (display dialog "${escaped}" with title "${title}" buttons {"Cancel", "Continue"} default button "Continue" cancel button "Cancel")`], { encoding: "utf8" });
return result.status === 0 && result.stdout.trim() === "Continue";
}
return spawnSync("zenity", ["--question", `--title=${title}`, `--text=${message}`]).status === 0;
}
async function readSyncConfiguration() {
const path = join(zedConfigDirectory(), ...LOCAL_SYNC_CONFIG.split("/"));
let source = {};
try {
source = JSON.parse(await readFile(path, "utf8"));
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
return {
path,
config: {
gist_id: typeof source.gist_id === "string" ? source.gist_id : "",
last_synced_hash: typeof source.last_synced_hash === "string" ? source.last_synced_hash : "",
},
};
}
async function saveSyncConfiguration(path, config) {
await writeAtomically(path, `${JSON.stringify(config, null, 2)}\n`);
}
function normalizeGistId(value) {
const matches = value.trim().match(/[a-f0-9]{8,}/gi);
if (!matches?.length) throw new Error("Enter a valid GitHub Gist ID or URL");
return matches.at(-1).toLowerCase();
}
async function promptForGist() {
const state = await readSyncConfiguration();
const entered = nativeInput(
"Paste the GitHub Gist ID or full Gist URL. The previous value is remembered on this machine.",
"Zed Settings Gist",
state.config.gist_id,
);
if (!entered) return null;
const gistId = normalizeGistId(entered);
if (gistId !== state.config.gist_id) {
state.config.gist_id = gistId;
state.config.last_synced_hash = "";
await saveSyncConfiguration(state.path, state.config);
}
return state;
}
function runGh(args, input) {
const result = spawnSync("gh", args, { encoding: "utf8", input, windowsHide: true });
if (result.error?.code === "ENOENT") {
throw new Error("GitHub CLI (gh) is not installed. Install it, then run: gh auth login");
}
if (result.status !== 0) throw new Error(result.stderr.trim() || `gh ${args[0]} failed`);
return result.stdout.trim();
}
function readRemoteGist(gistId) {
const gist = JSON.parse(runGh(["api", `gists/${gistId}`]));
const file = gist.files?.[BUNDLE_NAME];
if (!file) return { gist, parsed: null, contents: null };
if (file.truncated) throw new Error(`${BUNDLE_NAME} is too large to read through the Gist API`);
return { gist, parsed: parseBundle(file.content, `Gist ${gistId}`), contents: file.content };
}
function updateRemoteGist(gistId, contents) {
const payload = JSON.stringify({ files: { [BUNDLE_NAME]: { content: contents } } });
return JSON.parse(runGh(["api", "--method", "PATCH", `gists/${gistId}`, "--input", "-"], payload));
}
function shortHash(hash) {
return hash ? hash.slice(0, 12) : "none";
}
async function configureGist() {
const state = await promptForGist();
if (!state) return;
nativeMessage(`Saved Gist ID:\n${state.config.gist_id}\n\nAuthentication is managed by GitHub CLI (gh).`);
}
async function pushGist() {
const state = await promptForGist();
if (!state) return;
const localBundle = await createBundle();
const remote = readRemoteGist(state.config.gist_id);
const remoteHash = remote.parsed?.hash || "";
const lastHash = state.config.last_synced_hash;
if (remoteHash && remoteHash !== localBundle.hash) {
const remoteChanged = !lastHash || remoteHash !== lastHash;
if (remoteChanged) {
const proceed = nativeConfirm(
`The Gist contains different settings.\n\nLocal: ${shortHash(localBundle.hash)}\nRemote: ${shortHash(remoteHash)}\nLast sync: ${shortHash(lastHash)}\n\nOverwrite the Gist with this machine's settings?`,
);
if (!proceed) return;
}
}
const updated = updateRemoteGist(state.config.gist_id, localBundle.contents);
state.config.last_synced_hash = localBundle.hash;
await saveSyncConfiguration(state.path, state.config);
nativeMessage(`Uploaded to Gist ${state.config.gist_id}\n\nSHA-256: ${localBundle.hash}\nUpdated: ${updated.updated_at || "unknown"}`);
}
async function pullGist() {
const state = await promptForGist();
if (!state) return;
const localBundle = await createBundle();
const remote = readRemoteGist(state.config.gist_id);
if (!remote.parsed) throw new Error(`Gist ${state.config.gist_id} does not contain ${BUNDLE_NAME}`);
if (localBundle.hash !== remote.parsed.hash) {
const localChanged = !state.config.last_synced_hash || localBundle.hash !== state.config.last_synced_hash;
if (localChanged) {
const proceed = nativeConfirm(
`This machine contains different settings.\n\nLocal: ${shortHash(localBundle.hash)}\nRemote: ${shortHash(remote.parsed.hash)}\nLast sync: ${shortHash(state.config.last_synced_hash)}\n\nReplace local settings with the Gist version? A backup will be created first.`,
);
if (!proceed) return;
}
await installBundleContents(remote.contents, `Gist ${state.config.gist_id}`);
}
state.config.last_synced_hash = remote.parsed.hash;
await saveSyncConfiguration(state.path, state.config);
nativeMessage(`Downloaded from Gist ${state.config.gist_id}\n\nSHA-256: ${remote.parsed.hash}\nRemote updated: ${remote.gist.updated_at || "unknown"}`);
}
async function showGistStatus() {
const state = await promptForGist();
if (!state) return;
const localBundle = await createBundle();
const remote = readRemoteGist(state.config.gist_id);
const remoteHash = remote.parsed?.hash || "";
const lastHash = state.config.last_synced_hash;
let status = "The Gist does not contain a Zed settings file.";
if (remoteHash === localBundle.hash) status = "Synchronized";
else if (!lastHash) status = "Not synchronized on this machine";
else if (localBundle.hash === lastHash) status = "Remote changes available";
else if (remoteHash === lastHash) status = "Local changes pending upload";
else status = "Conflict: local and remote settings both changed";
nativeMessage(
`Gist: ${remote.gist.html_url || state.config.gist_id}\nStatus: ${status}\n\nLocal: ${shortHash(localBundle.hash)}\nRemote: ${shortHash(remoteHash)}\nLast sync: ${shortHash(lastHash)}\nRemote updated: ${remote.gist.updated_at || "unknown"}`,
"Zed Settings Gist Status",
);
}
async function main() {
const [mode, explicitPath] = process.argv.slice(2);
if (mode === "export") return exportBundle(resolve(explicitPath));
if (mode === "import") return importBundle(resolve(explicitPath));
if (mode === "export-dialog") {
const path = nativeFileDialog("save");
if (path) await exportBundle(path);
return;
}
if (mode === "import-dialog") {
const path = nativeFileDialog("open");
if (path) await importBundle(path);
return;
}
if (mode === "gist-configure") return configureGist();
if (mode === "gist-status") return showGistStatus();
if (mode === "gist-push") return pushGist();
if (mode === "gist-pull") return pullGist();
throw new Error(`Unknown mode: ${mode || "<missing>"}`);
}
try {
await main();
} catch (error) {
console.error(error.stack || error.message);
try {
nativeMessage(error.message, "Zed Settings Sync Error", true);
} catch {
// The terminal output remains available when the native dialog backend is unavailable.
}
process.exitCode = 1;
}
+138
View File
@@ -0,0 +1,138 @@
// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p)
{
"cli_default_open_behavior": "existing_window",
"icon_theme": "Material Icon Theme",
"auto_install_extensions": {
"dockerfile": true,
"env": true,
"html": true,
"material-icon-theme": true,
"mcp-server-context7": true,
"mcp-server-github": true,
"one-dark-pro": true,
"powershell": true,
"toml": true
},
"disable_ai": false,
"proxy": "",
"agent": {
"sidebar_side": "right",
"favorite_models": [],
"model_parameters": []
},
"project_panel": {
"dock": "left"
},
"telemetry": {
"diagnostics": false,
"metrics": false,
"anthropic_retention": false
},
"terminal": {
"font_size": 16.0,
"cursor_shape": "bar"
},
"base_keymap": "VSCode",
"file_scan_exclusions": [
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
"**/.classpath",
"**/.settings",
"**/.playwright-cli",
"**/output",
"**/.cursor",
"**/.vscode",
"**/.idea",
"**/.fleet",
"**/node_modules",
"**/bun.lockb",
"**/pnpm-lock.yaml",
"**/package-lock.json",
"**/yarn.lock",
"**/dist",
"**/out",
"**/build",
"**/.next",
"**/.nuxt",
"**/.output",
"**/.cache",
"**/.turbo",
"**/.vercel",
"**/.netlify",
"**/tsconfig.tsbuildinfo",
"**/auto-imports.d.ts",
"**/components.json",
"**/unplugin-vue-components.d.ts",
"src/*.d.ts",
"**/__pycache__",
"**/__init__.py",
"**/.ipynb_checkpoints",
"**/.pytest_cache",
"**/.mypy_cache",
"**/.venv",
"**/env",
"**/venv",
"**/.github",
"**/.husky",
"**/.npmrc",
"**/.eslintrc*",
"**/.prettier*",
"**/.editorconfig",
"**/.dockerignore",
"**/docker-compose.override.yml",
"**/cypress*",
"**/prisma/migrations"
],
"preview_tabs": {
"enabled": false
},
"git_panel": {
"fallback_branch_name": "master"
},
"sticky_scroll": {
"enabled": true
},
"minimap": {
"show": "always"
},
"hover_popover_delay": 250,
"cursor_blink": true,
"autosave": "on_focus_change",
"buffer_font_fallbacks": [
"Consolas",
"Sarasa Gothic SC",
"Courier New",
"monospace"
],
"buffer_font_family": "Fira Code",
"auto_indent_on_paste": true,
"linked_edits": true,
"show_edit_predictions": true,
"line_ending": "prefer_lf",
"hard_tabs": false,
"tab_size": 2,
"agent_servers": {
"codex-acp": {
"type": "registry"
}
},
"ui_font_size": 16,
"buffer_font_size": 16.0,
"theme": {
"mode": "system",
"light": "One Light",
"dark": "One Dark Pro",
},
}
+169
View File
@@ -0,0 +1,169 @@
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
//
// Example:
[
/* The generated example task is intentionally disabled.
{
"label": "Example task",
"command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done",
//"args": [],
// Env overrides for the command, will be appended to the terminal's environment from the settings.
"env": { "foo": "bar" },
// Current working directory to spawn the command into, defaults to current project root.
//"cwd": "/path/to/working/directory",
// Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
"use_new_terminal": false,
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
"reveal": "always",
// Where to place the task's terminal item after starting the task:
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
// * `center` — in the central pane group, "main" editor area
"reveal_target": "dock",
// What to do with the terminal pane and tab, after the command had finished:
// * `never` — Do nothing when the command finishes (default)
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`
"hide": "never",
// Which shell to use when running a task inside the terminal.
// May take 3 values:
// 1. (default) Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "args": ["--login"]
// }
// }
"shell": "system",
// Whether to show the task line in the output of the spawned task, defaults to `true`.
"show_summary": true,
// Whether to show the command line in the output of the spawned task, defaults to `true`.
"show_command": true,
// Which edited buffers to save before running the task:
// * `all` — save all edited buffers
// * `current` — save currently active buffer only
// * `none` — don't save any buffers
"save": "none",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
// "tags": []
}, */
{
"label": "Toggle excluded files",
"command": "node",
"args": [
"__TOGGLE_SCRIPT__"
],
"use_new_terminal": false,
"allow_concurrent_runs": false,
"reveal": "never",
"hide": "always",
"show_summary": false,
"show_command": false,
"save": "none"
},
{
"label": "Zed Settings: Export JSON...",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"export-dialog"
],
"allow_concurrent_runs": false,
"reveal": "never",
"hide": "always",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Import JSON...",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"import-dialog"
],
"allow_concurrent_runs": false,
"reveal": "never",
"hide": "always",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Configure GitHub Gist...",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"gist-configure"
],
"allow_concurrent_runs": false,
"reveal": "never",
"hide": "always",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Show GitHub Gist Status",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"gist-status"
],
"allow_concurrent_runs": false,
"reveal": "never",
"hide": "always",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Push to GitHub Gist",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"gist-push"
],
"allow_concurrent_runs": false,
"reveal": "no_focus",
"hide": "on_success",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Pull from GitHub Gist",
"command": "node",
"args": [
"__SYNC_SCRIPT__",
"gist-pull"
],
"allow_concurrent_runs": false,
"reveal": "no_focus",
"hide": "on_success",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Authenticate GitHub...",
"command": "gh",
"args": ["auth", "login"],
"allow_concurrent_runs": false,
"reveal": "always",
"hide": "never",
"show_summary": false,
"show_command": true,
"save": "none"
},
]