A visual web editor for iTerm2 Dynamic Profiles JSON files. Upload your profiles, preview color themes, edit settings, and download the modified file. Everything runs entirely in your browser.
Live site: https://iterm2-profile-editor.agagroup.workers.dev
- Visual preview — Terminal mockup cards with background/foreground colors, ANSI color swatches, and edit/duplicate overlays on hover
- Color theme presets — Apply Dracula, Solarized, Nord, Monokai, One Dark, Gruvbox, Tokyo Night, or Catppuccin Mocha with one click
- Color space preservation — Supports sRGB, Calibrated, and Display P3 color spaces from iTerm2
- Profile management — Add, duplicate, delete, and organize profiles by tags with collapsible groups
- Full settings editor — Edit colors, fonts, terminal size, cursor, scrollback, commands, and more with input validation
- Bulk operations — Select multiple profiles to apply themes, add tags, or delete in batch with a sticky action bar
- Credential masking — Passwords, tokens, and API keys in commands are automatically masked in preview cards
- Dark/light mode — Toggle between themes with system preference detection
- Auto-save — Changes persist to browser localStorage with debounced saves and restore on next visit
- Toast notifications — Visual feedback for all actions (upload, delete, bulk operations, save errors)
- Keyboard accessible — Full keyboard navigation for upload zone, profile selection, and actions
- Fully client-side — No server, no uploads, no tracking. Your data never leaves your browser.
pnpm install
pnpm devpnpm build
pnpm previewpnpm build
pnpm dlx wrangler deploy-
Export your profiles from iTerm2:
- iTerm2 → Settings → Profiles → Other Actions → Save All Profiles as JSON
- Or find them in
~/Library/Application Support/iTerm2/DynamicProfiles/
-
Upload the JSON file (drag & drop or browse)
-
Edit — click any profile card to open the full editor with three tabs:
- Colors — 21 color slots with pickers, apply presets, preserves original color space
- Settings — Name, tags, font, terminal size, cursor, scrollback, transparency
- Command — Custom shell command, session close behavior
-
Download the modified JSON and place it back in iTerm2's DynamicProfiles folder
If migrating from macOS Terminal, you can convert your profiles to iTerm2 format using a Python script that reads the Terminal.app plist and decodes the binary NSColor/NSFont data via the native macOS AppKit bridge.
Prerequisites: macOS with Python 3 and PyObjC (ships with the system Python on macOS, or install via pip install pyobjc).
Steps:
- Save the conversion script as
convert_terminal_to_iterm2.py:
#!/usr/bin/env python3
"""Convert macOS Terminal.app profiles to iTerm2 profiles."""
import json, plistlib, uuid, sys, os
import AppKit # noqa: F401
from Foundation import NSKeyedUnarchiver, NSData
from AppKit import NSColor, NSFont, NSColorSpace
def decode_nscolor(data_bytes):
"""Decode NSKeyedArchiver-encoded NSColor to RGB dict."""
try:
nsdata = NSData.dataWithBytes_length_(data_bytes, len(data_bytes))
color = NSKeyedUnarchiver.unarchiveObjectWithData_(nsdata)
if color is None:
return None
for method in ["NSCalibratedRGBColorSpace", "NSDeviceRGBColorSpace"]:
try:
rgb = color.colorUsingColorSpaceName_(method)
if rgb:
return {"Red Component": float(rgb.redComponent()),
"Green Component": float(rgb.greenComponent()),
"Blue Component": float(rgb.blueComponent()),
"Alpha Component": float(rgb.alphaComponent()),
"Color Space": "sRGB"}
except Exception:
pass
try:
rgb = color.colorUsingColorSpace_(NSColorSpace.sRGBColorSpace())
if rgb:
return {"Red Component": float(rgb.redComponent()),
"Green Component": float(rgb.greenComponent()),
"Blue Component": float(rgb.blueComponent()),
"Alpha Component": float(rgb.alphaComponent()),
"Color Space": "sRGB"}
except Exception:
pass
except Exception as e:
print(f" Warning: Could not decode color: {e}", file=sys.stderr)
return None
def decode_font(data_bytes):
"""Decode NSKeyedArchiver-encoded NSFont to name and size."""
try:
nsdata = NSData.dataWithBytes_length_(data_bytes, len(data_bytes))
font = NSKeyedUnarchiver.unarchiveObjectWithData_(nsdata)
if font:
return str(font.fontName()), float(font.pointSize())
except Exception as e:
print(f" Warning: Could not decode font: {e}", file=sys.stderr)
return None, None
def terminal_to_iterm2_profile(name, settings):
"""Convert a single Terminal.app profile to iTerm2 format."""
profile = {"Name": name, "Guid": str(uuid.uuid4()).upper(),
"Dynamic Profile Parent Name": "Default"}
color_map = {
"BackgroundColor": "Background Color", "TextColor": "Foreground Color",
"TextBoldColor": "Bold Color", "CursorColor": "Cursor Color",
"SelectionColor": "Selection Color",
"ANSIBlackColor": "Ansi 0 Color", "ANSIRedColor": "Ansi 1 Color",
"ANSIGreenColor": "Ansi 2 Color", "ANSIYellowColor": "Ansi 3 Color",
"ANSIBlueColor": "Ansi 4 Color", "ANSIMagentaColor": "Ansi 5 Color",
"ANSICyanColor": "Ansi 6 Color", "ANSIWhiteColor": "Ansi 7 Color",
"ANSIBrightBlackColor": "Ansi 8 Color", "ANSIBrightRedColor": "Ansi 9 Color",
"ANSIBrightGreenColor": "Ansi 10 Color", "ANSIBrightYellowColor": "Ansi 11 Color",
"ANSIBrightBlueColor": "Ansi 12 Color", "ANSIBrightMagentaColor": "Ansi 13 Color",
"ANSIBrightCyanColor": "Ansi 14 Color", "ANSIBrightWhiteColor": "Ansi 15 Color",
}
for term_key, iterm_key in color_map.items():
if term_key in settings:
color = decode_nscolor(settings[term_key])
if color:
profile[iterm_key] = color
if "Font" in settings:
font_name, font_size = decode_font(settings["Font"])
if font_name and font_size:
profile["Normal Font"] = f"{font_name} {font_size}"
profile["Non Ascii Font"] = f"{font_name} {font_size}"
if "columnCount" in settings:
profile["Columns"] = int(settings["columnCount"])
if "rowCount" in settings:
profile["Rows"] = int(settings["rowCount"])
if "CommandString" in settings:
profile["Custom Command"] = "Yes"
profile["Command"] = settings["CommandString"]
else:
profile["Custom Command"] = "No"
return profile
def main():
plist_path = os.path.expanduser(
"~/Library/Preferences/com.apple.Terminal.plist")
if not os.path.exists(plist_path):
print(f"Error: {plist_path} not found", file=sys.stderr)
sys.exit(1)
with open(plist_path, "rb") as f:
plist = plistlib.load(f)
window_settings = plist.get("Window Settings", {})
print(f"Found {len(window_settings)} Terminal.app profiles")
profiles = []
for name, settings in sorted(window_settings.items()):
print(f" Converting: {name}")
profiles.append(terminal_to_iterm2_profile(name, settings))
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"terminal-profiles-for-iterm2.json")
with open(output_path, "w") as f:
json.dump({"Profiles": profiles}, f, indent=2)
print(f"\nWrote {len(profiles)} profiles to: {output_path}")
if __name__ == "__main__":
main()- Run the script:
python3 convert_terminal_to_iterm2.py- Upload the generated
terminal-profiles-for-iterm2.jsonto this editor
- SvelteKit 2 + Svelte 5 (runes mode)
- Tailwind CSS 4 via
@tailwindcss/vite - shadcn-svelte (bits-ui)
- svelte-sonner (toast notifications)
- Cloudflare Workers via
@sveltejs/adapter-cloudflare
All processing happens entirely in your browser. No data is sent to any server. Profiles are cached in localStorage with auto-save and auto-restore. Passwords and sensitive tokens in command strings are masked with asterisks in preview cards — the editor textarea shows actual values since you need to edit them. A beforeunload warning prevents accidental data loss when closing the tab.
