felixse/FluentTerminal · error · ParseThemeException

Root node was not a dictionary.

Error message

Root node was not a dictionary.

What it means

Thrown by ITermThemeParser.Parse when PList.Load(fileContent) returns a node that is not a DictionaryNode. iTerm .itermcolors files are Apple property-list files whose root must be a dictionary keyed by color names (e.g. "Ansi 0 Color"). If the deserialized root is an array, string, data, or any non-dict PNode, the `as DictionaryNode` cast yields null and the ?? coalesce throws ParseThemeException.

Source

Thrown at FluentTerminal.App.Services/Implementation/ITermThemeParser.cs:67

        {
            public const string BlueComponent = "Blue Component";
            public const string GreenComponent = "Green Component";
            public const string RedComponent = "Red Component";
        }

        public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
        {
            if (string.IsNullOrWhiteSpace(fileName))
            {
                throw new ArgumentNullException(nameof(fileName));
            }

            if (fileContent == null)
            {
                throw new ArgumentNullException(nameof(fileContent));
            }

            var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");

            return Task.FromResult(new TerminalTheme
            {
                Name = Path.GetFileNameWithoutExtension(fileName),
                Colors = GetColors(node),
                Id = Guid.NewGuid(),
                PreInstalled = false
            });
        }

        private TerminalColors GetColors(DictionaryNode themeDictionary)
        {
            return new TerminalColors
            {
                Background = GetColorString(themeDictionary[ITermThemeKeys.BackgroundColor]),
                Foreground = GetColorString(themeDictionary[ITermThemeKeys.ForegroundColor]),
                Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
                CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),

View on GitHub (pinned to ba83ec485e)

Solutions

  1. Validate the file is a real iTerm theme before parsing: open it in a text editor (XML plists) and confirm the top-level <dict> ... </dict>; re-export from iTerm2 if missing.
  2. Catch ParseThemeException around the Parse/Import call and show the user a friendly 'invalid theme file' message instead of crashing.
  3. If authoring themes programmatically, ensure the serialized root is a DictionaryNode (PListNet.Nodes.DictionaryNode) before writing.

Example fix

// before
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");

// after - surface the actual root type to aid debugging
var loaded = PList.Load(fileContent);
var node = loaded as DictionaryNode;
if (node == null)
    throw new ParseThemeException($"Root node was not a dictionary (was {loaded?.GetType().Name ?? "null"}).");
Defensive patterns

Strategy: validation

Validate before calling

// Pre-validate the plist stream is a dict before handing off to the parser.
using (var reader = new StreamReader(fileContent)) { /* peek not trivial for binary plist */ }
// Easiest: validate after load via PListNet and reject early:
var loaded = PList.Load(fileContent);
if (!(loaded is PListNet.Nodes.DictionaryNode))
    return Task.FromException<TerminalTheme>(
        new ParseThemeException("Theme root must be a dictionary."));

Type guard

static bool IsDictionaryRoot(PNode node) => node is PListNet.Nodes.DictionaryNode;

Try / catch

try { theme = await parser.Parse(name, stream); }
catch (ParseThemeException ex) { logger.Warn(ex, "Invalid theme {Name}"); notifyUser("Theme file is not a valid iTerm theme."); }

Prevention

When it happens

Trigger: Calling Parse(fileName, stream) with a stream whose contents are not a plist dictionary: an empty file, a plist whose root is an <array>, a JSON plist that is a list, a binary plist with a non-dict top object, or a completely different file format passed with a .itermcolors extension.

Common situations: User downloads or hand-edits a .itermcolors theme and saves it as an array or strips the root dict; a theme exported by a different terminal (not iTerm) is renamed to .itermcolors; the file is truncated/corrupted during download so PListNet falls back to a non-dict node.

Related errors


AI-assisted analysis of felixse/FluentTerminal@ba83ec485e (2026-08-13). Data as JSON: /api/errors/fb1be4422fc0970d. Report an issue: GitHub.