Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active February 21, 2026 12:39
Show Gist options
  • Select an option

  • Save ttscoff/58a3f7d69fff63caa11766f23647f888 to your computer and use it in GitHub Desktop.

Select an option

Save ttscoff/58a3f7d69fff63caa11766f23647f888 to your computer and use it in GitHub Desktop.
Convert iThoughts X mind map to Mermaid diagram. Save ithougts.md and python file to .cursor/mermaid, and ithoughts-to-mermaid.md to .cursor/commands

iThoughts to Mermaid Mindmap (Cursor command)

Convert an iThoughts .itmz mind map file into a Mermaid mindmap for display in Cursor.

Instructions

  1. Get the itmz path: The user may provide a path (e.g. from iCloud: ~/Library/Mobile Documents/iCloud~com~toketaware~ios~ithoughts/Documents/Map Name.itmz). If no path is given, ask for it.

  2. Run the converter:

    python3 .cursor/mermaid/ithoughts_to_mermaid.py "<path-to-itmz>"
  3. Output:

    • Insert the resulting Mermaid block into the active document, wrapped in ```mermaid ... ```, or
    • If the user wants a file: python3 .cursor/mermaid/ithoughts_to_mermaid.py "<path>" -o output.md
  4. Format reference: See .cursor/mermaid/ithoughts.md for itmz structure. The script uses mapdata.xml for the outline and style.xml for per-level topic shapes (rounded, oval, hexagon, etc.).

#!/usr/bin/env python3
"""
Convert iThoughts .itmz mind map to Mermaid mindmap format.
Usage:
python ithoughts_to_mermaid.py <path-to-file.itmz>
python ithoughts_to_mermaid.py <path-to-file.itmz> -o output.md
Output is written to stdout or the specified file.
"""
import argparse
import re
import sys
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
# topicShape → Mermaid mindmap node delimiters
# 0=rounded, 1=oval, 2=rectangle, 3=hexagon
SHAPE_DELIMITERS = {
0: ("(", ")"), # rounded (text)
1: ("((", "))"), # oval/circle ((text))
2: ("[", "]"), # square [text]
3: ("{{", "}}"), # hexagon {{text}}
}
DEFAULT_SHAPE = (None, None) # no delimiters = default node
# Trailing typo patterns: delimiter + stray backtick or vice versa (e.g. "Editor`)")
_TYPO_SUFFIXES = (")`", "]`", "}`", "`)", "`]", "`}")
def sanitize_text(text: str) -> str:
"""Remove common typos from source map labels that would break Mermaid parsing."""
s = text.strip()
# Strip stray leading/trailing backticks
s = s.strip("`")
# Remove typo suffixes (e.g. "Editor`)" or "Item)`")
changed = True
while changed and s:
changed = False
for suffix in _TYPO_SUFFIXES:
if s.endswith(suffix):
s = s[: -len(suffix)].rstrip()
changed = True
break
# Strip lone trailing ) ] } when unbalanced (common typo)
for open_c, close_c in [("(", ")"), ("[", "]"), ("{", "}")]:
while s.endswith(close_c) and s.count(open_c) < s.count(close_c):
s = s[:-1].rstrip()
# Collapse multiple spaces
s = re.sub(r" +", " ", s)
return s
def parse_style(zf) -> dict[int, int]:
"""Parse style.xml, return level n -> topicShape."""
shapes = {}
try:
with zf.open("style.xml") as f:
content = f.read().decode("utf-8", errors="replace")
except KeyError:
return shapes
root = ET.fromstring(content)
for level in root.findall(".//level"):
n = level.get("n")
ts = level.get("topicShape")
if n is not None and ts is not None:
try:
shapes[int(n)] = int(ts)
except ValueError:
pass
return shapes
_BRACKET_CHARS = frozenset("(){}[]")
def wrap_text(text: str, shape_key: int, shape_map: dict) -> str:
"""Apply Mermaid shape delimiters to node text. If content contains any
bracket chars ( ) { } [ ], wrap the content in double quotes."""
shape = shape_map.get(shape_key, 0)
left, right = SHAPE_DELIMITERS.get(shape, DEFAULT_SHAPE)
content = text
if any(c in text for c in _BRACKET_CHARS):
# Double the quote to escape ("" not \")
content = '"' + text.replace('"', '""') + '"'
if left and right:
return f"{left}{content}{right}"
return content
def escape_mermaid(text: str) -> str:
"""Escape text for Mermaid mindmap (no shape delimiters)."""
return (
text.replace("]", "\\]")
.replace("[", "\\[")
.replace("(", "\\(")
.replace(")", "\\)")
.replace("{", "\\{")
.replace("}", "\\}")
.replace('"', '\\"')
)
def topic_to_mermaid(
topic: ET.Element,
shapes: dict[int, int],
level: int,
indent: str,
out: list[str],
) -> None:
text_attr = sanitize_text(topic.get("text", ""))
if not text_attr:
text_attr = "(unnamed)"
shape_key = min(level, max(shapes.keys(), default=0)) if shapes else 0
if shapes:
# Use shape for this level
wrapped = wrap_text(text_attr, shape_key, shapes)
else:
wrapped = escape_mermaid(text_attr)
out.append(f"{indent}{wrapped}")
child_indent = indent + " "
for child in topic:
if child.tag == "topic":
topic_to_mermaid(child, shapes, level + 1, child_indent, out)
def extract_root_topic(root: ET.Element) -> ET.Element | None:
"""Get the root topic. mapdata has <topics> (inside <iThoughts> or at root)."""
topics = root.find("topics")
if topics is None:
topics = root
for c in topics:
if c.tag == "topic":
return c
return None
def convert(itmz_path: str) -> str:
lines = ["mindmap"]
with zipfile.ZipFile(itmz_path, "r") as zf:
shapes = parse_style(zf)
with zf.open("mapdata.xml") as f:
content = f.read().decode("utf-8", errors="replace")
root = ET.fromstring(content)
topic = extract_root_topic(root)
if topic is None:
return "mindmap\n (empty map)\n"
topic_to_mermaid(topic, shapes, 0, " ", lines)
return "\n".join(lines) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description="Convert iThoughts .itmz to Mermaid mindmap")
parser.add_argument("itmz", help="Path to .itmz file")
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
args = parser.parse_args()
try:
result = convert(args.itmz)
if args.output:
Path(args.output).write_text(result, encoding="utf-8")
else:
print(result, end="")
except zipfile.BadZipFile as e:
print(f"Error: Not a valid zip/itmz file: {e}", file=sys.stderr)
sys.exit(1)
except ET.ParseError as e:
print(f"Error: Invalid XML in mapdata.xml: {e}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print(f"Error: File not found: {args.itmz}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

iThoughts ITMZ Format Reference

iThoughts mind maps are stored as .itmz files: ZIP archives containing XML and other assets.

File Structure

File Description
mapdata.xml Topic hierarchy, text, positions, links
style.xml Per-level topic shapes, colors, fonts
preview.png Thumbnail preview
manifest.plist, preferences.plist, display_state.plist Metadata

mapdata.xml

Root element: <iThoughts><topics>...</topics></iThoughts> (or just <topics> in some exports).

Topic element

<topic uuid="..." position="{x,y}" text="Topic text" text-size="20" created="..." modified="..." link="https://...">
  <topic uuid="..." ...>...</topic>  <!-- child topics -->
</topic>
  • uuid: Unique identifier
  • position: {x,y} canvas coordinates (used for layout; hierarchy is from nesting)
  • text: Node label (XML-escaped: &amp; &lt; &gt; &quot; &apos;)
  • text-size: Font size (optional)
  • link: URL (optional)
  • note: Note/description (optional)
  • summary1/summary2: UUIDs for summary brackets (optional)

Hierarchy: Parent-child relationships are defined by XML nesting. The first <topic> at the root is the center; its direct children are branches; their nested <topic> elements are sub-branches, and so on.

style.xml

<style version="2" name="..." ...>
  <level n="0" topicShape="1" topicColor="FFFFFF" ... />
  <level n="1" topicShape="0" topicColor="FF7F00" ... />
  <level n="2" topicShape="3" topicColor="929292" ... />
</style>

topicShape (per level)

Shape applied to topics at that depth. Mermaid mindmap mapping:

topicShape iThoughts shape (approx) Mermaid syntax
0 Rounded rectangle (text)
1 Oval/ellipse ((text))
2 Rectangle [text]
3 Hexagon {{text}}
4+ (other shapes) default (no delimiters)

Levels beyond defined <level> elements inherit from the last defined level.

Mermaid mindmap output

  • Root node: typically ((text)) for center
  • Children: indented 4 spaces per level
  • Node text: escape [ ] ( ) { } " in labels; wrap in quotes if needed
  • Links: Mermaid supports [text](url) in some contexts; for plain mindmap, link may be omitted or appended as text
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment