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
pnpm install
pnpm dev
pnpm build
pnpm preview
pnpm build
pnpm dlx wrangler deploy
Export your profiles from iTerm2:
~/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:
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:
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()
python3 convert_terminal_to_iterm2.py
terminal-profiles-for-iterm2.json to this editor@tailwindcss/vite@sveltejs/adapter-cloudflareAll 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.