{"record":{"id":"7817ef8cea248a1c","repo":"felixse/FluentTerminal","slug":"color-node-was-not-a-dictionary","errorCode":null,"errorMessage":"Color node was not a dictionary","messagePattern":"Color node was not a dictionary","errorType":"exception","errorClass":"ParseThemeException","httpStatus":null,"severity":"error","filePath":"FluentTerminal.App.Services/Implementation/ITermThemeParser.cs","lineNumber":110,"sourceCode":"                Yellow = GetColorString(themeDictionary[ITermThemeKeys.Ansi3Color]),\n                Blue = GetColorString(themeDictionary[ITermThemeKeys.Ansi4Color]),\n                Magenta = GetColorString(themeDictionary[ITermThemeKeys.Ansi5Color]),\n                Cyan = GetColorString(themeDictionary[ITermThemeKeys.Ansi6Color]),\n                White = GetColorString(themeDictionary[ITermThemeKeys.Ansi7Color]),\n                BrightBlack = GetColorString(themeDictionary[ITermThemeKeys.Ansi8Color]),\n                BrightRed = GetColorString(themeDictionary[ITermThemeKeys.Ansi9Color]),\n                BrightGreen = GetColorString(themeDictionary[ITermThemeKeys.Ansi10Color]),\n                BrightYellow = GetColorString(themeDictionary[ITermThemeKeys.Ansi11Color]),\n                BrightBlue = GetColorString(themeDictionary[ITermThemeKeys.Ansi12Color]),\n                BrightMagenta = GetColorString(themeDictionary[ITermThemeKeys.Ansi13Color]),\n                BrightCyan = GetColorString(themeDictionary[ITermThemeKeys.Ansi14Color]),\n                BrightWhite = GetColorString(themeDictionary[ITermThemeKeys.Ansi15Color]),\n            };\n        }\n\n        private string GetColorString(PNode colorNode, byte alpha = Byte.MaxValue)\n        {\n            var dictionaryNode = colorNode as DictionaryNode ?? throw new ParseThemeException(\"Color node was not a dictionary\");\n\n            var red = dictionaryNode[ITermThemeColorKeys.RedComponent] as RealNode ?? throw new ParseThemeException(\"Red node value was not a real number\");\n            var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException(\"Green node value was not a real number\");\n            var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException(\"Blue node value was not a real number\");\n\n            if (alpha == byte.MaxValue)\n            {\n                return $\"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}\";\n            }\n            else\n            {\n                return $\"rgba({GetByteValue(red):G}, {GetByteValue(green):G}, {GetByteValue(blue):G}, {ToDoubleString(alpha)})\";\n            }\n        }\n\n        private byte GetByteValue(RealNode node)\n        {\n            var doubleValue = node.Value * Byte.MaxValue;","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/felixse/FluentTerminal/blob/ba83ec485eea8d8ee65825711731300bee2e76ed/FluentTerminal.App.Services/Implementation/ITermThemeParser.cs#L92-L128","documentation":"Thrown inside GetColorString when an individual color entry (e.g. \"Ansi 0 Color\", \"Background Color\") is not itself a DictionaryNode. In iTerm themes each color is a dictionary containing \"Red Component\", \"Green Component\", \"Blue Component\" keys. If the entry is a scalar RealNode, string, or data node, the `colorNode as DictionaryNode` cast is null and ParseThemeException is thrown.","triggerScenarios":"GetColors iterates every themeDictionary[ITermThemeKeys.*] color key and calls GetColorString. If any single color entry is a plain number/string instead of a dict (e.g. someone replaced a color sub-dict with a hex string \"#ff0000\"), the cast fails.","commonSituations":"Theme was converted by a tool that flattened colors to scalar hex strings; a manually edited plist where one color entry lost its nested <dict>; a partially-compatible iTerm theme variant.","solutions":["Open the .itermcolors file and verify every \"Ansi N Color\" / \"Background Color\" / \"Foreground Color\" entry is a <dict> containing Red/Green/Blue Component keys.","Re-export the theme from iTerm2 to regenerate the correct nested-dict structure.","Catch ParseThemeException at the call site and report which theme failed so the user can replace it."],"exampleFix":"// before\nvar dictionaryNode = colorNode as DictionaryNode ?? throw new ParseThemeException(\"Color node was not a dictionary\");\n\n// after - tolerate scalar colors and skip, logging the bad entry\nif (!(colorNode is DictionaryNode dictionaryNode))\n    throw new ParseThemeException($\"Color node was not a dictionary (got {colorNode?.GetType().Name})\");","handlingStrategy":"type-guard","validationCode":"// After loading the root dict, check each color entry is a dict before GetColorString.\nforeach (var key in new[] { \"Background Color\", \"Ansi 0 Color\" /*...*/ })\n{\n    if (node[key] is PListNet.Nodes.DictionaryNode)\n        continue;\n    return Task.FromException<TerminalTheme>(new ParseThemeException($\"{key} is not a dictionary\"));\n}","typeGuard":"static bool IsColorDictionary(PNode n) => n is PListNet.Nodes.DictionaryNode d\n    && d.ContainsKey(\"Red Component\") && d.ContainsKey(\"Green Component\") && d.ContainsKey(\"Blue Component\");","tryCatchPattern":"try { theme = await parser.Parse(name, stream); }\ncatch (ParseThemeException ex) when (ex.Message.Contains(\"Color node\"))\n{ notifyUser(\"Theme has a malformed color entry.\"); }","preventionTips":["Use themes exported directly by iTerm2 to guarantee nested color dicts.","Add a structural validator that checks every expected color key is a dict before parsing.","Catch ParseThemeException at import time and skip/flag the bad theme."],"tags":["theme-parsing","plist","iterm","color","input-validation"],"backgroundTag":null,"analyzedSha":"ba83ec485eea8d8ee65825711731300bee2e76ed","analyzedAt":"2026-08-13T21:23:15.467Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}