diff options
| author | historia <[not public]> | 2026-07-18 04:44:50 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-18 04:44:50 -0400 |
| commit | 2b5d8f595f96a2864899f4c7e28bc06fa52d2603 (patch) | |
| tree | f1539db865d9968414e59d9b250861c1b356d627 /internal/admin/api_colors.go | |
| parent | db8ca1fe26a36b78074a424ae657b2cbb9b3af19 (diff) | |
| download | thehouseoficarus-2b5d8f595f96a2864899f4c7e28bc06fa52d2603.tar.gz | |
feat: expose all colors in config.yaml for easier theming
Diffstat (limited to 'internal/admin/api_colors.go')
| -rw-r--r-- | internal/admin/api_colors.go | 373 |
1 files changed, 289 insertions, 84 deletions
diff --git a/internal/admin/api_colors.go b/internal/admin/api_colors.go index 882a096..00caa98 100644 --- a/internal/admin/api_colors.go +++ b/internal/admin/api_colors.go @@ -10,6 +10,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/config" + "thehouseoficarus/internal/game" ) // colorCategory describes one editable color category: its config key, a @@ -38,77 +39,132 @@ type colorSection struct { Lines []colorLine `json:"lines"` } -// colorCategories is the curated, grouped, ordered master list mirroring the -// keys produced by config.DefaultColors(). Editing here is the single source -// of truth for the admin color themer's left-side list. -var colorCategories = []colorCategory{ - // Room / look - {Name: "room_name", Label: "Room Name", Group: "Room"}, - {Name: "room_number", Label: "Room Number", Group: "Room"}, - {Name: "room_desc", Label: "Room Description", Group: "Room"}, - {Name: "exit_direction", Label: "Exit Direction", Group: "Room"}, - {Name: "exit_name", Label: "Exit Name", Group: "Room"}, - {Name: "mob", Label: "Mob", Group: "Room"}, - {Name: "protected_mob", Label: "Protected Mob", Group: "Room"}, - {Name: "player_name", Label: "Player Name", Group: "Room"}, - {Name: "item", Label: "Item", Group: "Room"}, - {Name: "dim", Label: "Dim / Brackets", Group: "Room"}, - - // Movement - {Name: "direction", Label: "Move Direction", Group: "Movement"}, - - // Combat - {Name: "damage_dealt", Label: "Damage Dealt", Group: "Combat"}, - {Name: "damage_taken", Label: "Damage Taken", Group: "Combat"}, - {Name: "miss", Label: "Miss", Group: "Combat"}, - {Name: "death", Label: "Death", Group: "Combat"}, - {Name: "victory", Label: "Victory", Group: "Combat"}, - {Name: "warning", Label: "Warning", Group: "Combat"}, - {Name: "error", Label: "Error", Group: "Combat"}, - {Name: "science_mod", Label: "Science Mod", Group: "Combat"}, - - // XP / level - {Name: "xp", Label: "XP Drop", Group: "XP / Level"}, - {Name: "level_up", Label: "Level Up", Group: "XP / Level"}, - - // Pickups / economy - {Name: "currency_pickup", Label: "Currency Pickup", Group: "Pickups / Economy"}, - - // Chat / dialog - {Name: "say", Label: "Say", Group: "Chat / Dialog"}, - {Name: "global", Label: "Global", Group: "Chat / Dialog"}, - {Name: "broadcast", Label: "Broadcast", Group: "Chat / Dialog"}, - {Name: "dialog", Label: "Dialog", Group: "Chat / Dialog"}, - {Name: "dialog_options", Label: "Dialog Options", Group: "Chat / Dialog"}, - - // Drops / consume / fire - {Name: "drop_message", Label: "Drop Message", Group: "Drops / Consume / Fire"}, - {Name: "eat_food", Label: "Eat Food", Group: "Drops / Consume / Fire"}, - {Name: "fire", Label: "Fire", Group: "Drops / Consume / Fire"}, - - // Tick / status - {Name: "visual_tick", Label: "Visual Tick", Group: "Tick / Status"}, - {Name: "visual_first_tick", Label: "Visual First Tick", Group: "Tick / Status"}, - - // Tech / farm - {Name: "battery", Label: "Battery", Group: "Tech / Farm"}, - {Name: "tech_depleted", Label: "Tech Depleted", Group: "Tech / Farm"}, - {Name: "farm_grow", Label: "Farm Grow", Group: "Tech / Farm"}, - {Name: "farm_disease", Label: "Farm Disease", Group: "Tech / Farm"}, - - // Assassin / map / safespot - {Name: "assassin_task", Label: "Assassin Task", Group: "Assassin / Map / Safespot"}, - {Name: "map_at", Label: "Map At (you)", Group: "Assassin / Map / Safespot"}, - {Name: "map_blocked", Label: "Map Blocked", Group: "Assassin / Map / Safespot"}, - {Name: "map_agility", Label: "Map Agility", Group: "Assassin / Map / Safespot"}, - {Name: "safespot_alert", Label: "Safespot Alert", Group: "Assassin / Map / Safespot"}, - - // System - {Name: "sequence", Label: "Sequence (trigger)", Group: "System"}, +// playerColorLabels maps each player-overridable color category name to the +// human-friendly label shown in the admin GUI. Order comes from the shared +// game.PlayerColorCategoryOrder() so the admin editor always mirrors the +// in-game `colors` command output exactly. +var playerColorLabels = map[string]string{ + "room_name": "Room Name", + "room_number": "Room Number", + "room_desc": "Room Description", + "exit_direction": "Exit Direction", + "exit_name": "Exit Name", + "dim": "Dim / Brackets", + "mob": "Mob", + "protected_mob": "Protected Mob", + "player_name": "Player Name", + "item": "Item", + "direction": "Move Direction", + "damage_dealt": "Damage Dealt", + "damage_taken": "Damage Taken", + "miss": "Miss", + "death": "Death", + "victory": "Victory", + "warning": "Warning", + "error": "Error", + "science_mod": "Science Mod", + "xp": "XP Drop", + "level_up": "Level Up", + "currency_pickup": "Currency Pickup", + "say": "Say", + "global": "Global", + "broadcast": "Broadcast", + "dialog": "Dialog", + "dialog_options": "Dialog Options", + "drop_message": "Drop Message", + "eat_food": "Eat Food", + "fire": "Fire", + "visual_tick": "Visual Tick", + "visual_first_tick": "Visual First Tick", + "battery": "Battery", + "tech_depleted": "Tech Depleted", + "farm_grow": "Farm Grow", + "farm_disease": "Farm Disease", + "assassin_task": "Assassin Task", + "map_at": "Map At (you)", + "map_blocked": "Map Blocked", + "map_agility": "Map Agility", + "safespot_alert": "Safespot Alert", + "sequence": "Sequence (trigger)", } -// knownColor returns true if name is one of the editable categories. -func knownColor(name string) bool { +// colorCategories is the ordered list of player-overridable color categories +// shown in the admin GUI. Built from game.PlayerColorCategoryOrder() so it +// stays in lock-step with the in-game `colors` command output. Group is left +// empty: the editor renders one flat list under the "Player Colors" heading. +var colorCategories = buildPlayerCategories() + +func buildPlayerCategories() []colorCategory { + order := game.PlayerColorCategoryOrder() + out := make([]colorCategory, 0, len(order)) + for _, name := range order { + label := playerColorLabels[name] + if label == "" { + label = name + } + out = append(out, colorCategory{Name: name, Label: label}) + } + return out +} + +// constantCategories is the ordered list of constant (player-invisible) color +// categories exposed in the admin GUI under the "Constants" heading. Order is +// family-grouped for skimmability; Group is currently unused by the client. +var constantCategories = []colorCategory{ + // HP / status bars + {Name: "hp_bar_high", Label: "HP Bar — High (>=70%)", Group: "Bars"}, + {Name: "hp_bar_med", Label: "HP Bar — Medium (>=40%)", Group: "Bars"}, + {Name: "hp_bar_low", Label: "HP Bar — Low (<40%)", Group: "Bars"}, + {Name: "hp_bar_empty", Label: "HP Bar — Empty Cells", Group: "Bars"}, + {Name: "task_bar_high", Label: "Task Bar — High (>=70%)", Group: "Bars"}, + {Name: "task_bar_med", Label: "Task Bar — Medium (>=40%)", Group: "Bars"}, + {Name: "task_bar_low", Label: "Task Bar — Low (<40%)", Group: "Bars"}, + {Name: "battery_bar", Label: "Battery Bar Fill", Group: "Bars"}, + {Name: "run_energy_bar", Label: "Run Energy Bar Fill", Group: "Bars"}, + + // Skill-level tier color on the score panel + {Name: "skill_lvl_max", Label: "Skill Level Color — 99", Group: "Skills"}, + {Name: "skill_lvl_high", Label: "Skill Level Color — 80+", Group: "Skills"}, + {Name: "skill_lvl_mid", Label: "Skill Level Color — 60+", Group: "Skills"}, + {Name: "skill_lvl_low", Label: "Skill Level Color — 40+", Group: "Skills"}, + {Name: "skill_lvl_base", Label: "Skill Level Color — base", Group: "Skills"}, + + // Level-difference bands + {Name: "level_diff_equal", Label: "Level Diff — Equal", Group: "Level Diff"}, + {Name: "level_diff_up_small", Label: "Level Diff — Slightly Higher (+1..+4)", Group: "Level Diff"}, + {Name: "level_diff_up_big", Label: "Level Diff — Much Higher (>=+5)", Group: "Level Diff"}, + {Name: "level_diff_down_small", Label: "Level Diff — Slightly Lower (-1..-4)", Group: "Level Diff"}, + {Name: "level_diff_down_big", Label: "Level Diff — Much Lower (<=-5)", Group: "Level Diff"}, + + // Tech list status / labels + {Name: "tech_status_on", Label: "Tech Status — ON", Group: "Tech"}, + {Name: "tech_status_off", Label: "Tech Status — off", Group: "Tech"}, + {Name: "tech_status_locked", Label: "Tech Status — locked", Group: "Tech"}, + {Name: "tech_name_unlocked", Label: "Tech Name — unlocked", Group: "Tech"}, + {Name: "tech_name_locked", Label: "Tech Name — locked", Group: "Tech"}, + {Name: "active_tech_on", Label: "Active Tech [ON] tag", Group: "Tech"}, + {Name: "active_tech_status", Label: "Active Buff [BUFF] tag", Group: "Tech"}, + {Name: "active_tech_stat", Label: "Tech / Buff Stat Name", Group: "Tech"}, + {Name: "score_max_value", Label: "Score Panel Max Value", Group: "Tech"}, + + // Shared table-heading chrome + {Name: "table_header_label", Label: "Table Header — Label", Group: "Tables"}, + {Name: "table_header_value", Label: "Table Header — Value", Group: "Tables"}, + {Name: "table_header_detail", Label: "Table Header — Detail", Group: "Tables"}, + {Name: "table_header_xp", Label: "Table Header — XP / Cost", Group: "Tables"}, + {Name: "table_header_desc", Label: "Table Header — Description", Group: "Tables"}, + {Name: "production_locked", Label: "Production Menu — Locked Row", Group: "Tables"}, + + // APS / look inline accents + {Name: "aps_here", Label: "APS Datapad — \"here\"", Group: "Accents"}, + {Name: "mob_hp_inline", Label: "Mob Inline HP", Group: "Accents"}, + {Name: "protected_label", Label: "Mob \"(protected)\" tag", Group: "Accents"}, + {Name: "god_tag_label", Label: "God-mode [LOCAL]/[GLOBAL]/(HIDDEN)", Group: "Accents"}, +} + +// knownPlayerColor returns true if name is one of the player-overridable +// categories. +func knownPlayerColor(name string) bool { for _, c := range colorCategories { if c.Name == name { return true @@ -117,6 +173,22 @@ func knownColor(name string) bool { return false } +// knownConstantColor returns true if name is one of the constant +// (admin-only) color categories. +func knownConstantColor(name string) bool { + for _, c := range constantCategories { + if c.Name == name { + return true + } + } + return false +} + +// knownColor returns true if name is editable in either section. +func knownColor(name string) bool { + return knownPlayerColor(name) || knownConstantColor(name) +} + // seg is a small constructor helper for the inline preview builder. func seg(t, cat string) colorSegment { return colorSegment{T: t, C: cat} @@ -157,12 +229,12 @@ func buildPreview() []colorSection { seg("║", "dim"), seg("■", ""), seg("-", ""), seg("@", "map_at"), seg("-", ""), seg("■", ""), seg("║", "dim")), ln(seg("Crassius Rex", "protected_mob"), seg(" watches the gate. ", ""), seg("║", "dim"), seg(" ", ""), seg("│", "map_agility"), seg(" ", ""), seg("║", "dim")), - ln(seg("A man ", "mob"), seg("(level 5)", "victory"), seg(" scribbles something in a small notebook. ", ""), + ln(seg("A man ", "mob"), seg("(level 5)", "level_diff_down_big"), seg(" scribbles something in a small notebook. ", ""), seg("║", "dim"), seg(" ", ""), seg("■", ""), seg(" ", ""), seg("║", "dim")), ln(seg(" ", ""), seg("╚═════╝", "dim")), txt(""), - ln(seg("Dalinar", "player_name"), seg(" (level 123)", "error"), seg(" is here.", "")), + ln(seg("Dalinar", "player_name"), seg(" (level 123)", "level_diff_equal"), seg(" is here.", "")), txt(""), txt("On the ground:"), ln(seg(" bones ", "item"), seg("(reserved for Dalinar 65t)", "dim")), @@ -191,19 +263,19 @@ func buildPreview() []colorSection { ln(seg("You attack ", ""), seg("the man", "mob"), seg("! Style: accurate (+3 atk)", "")), ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""), seg("\U0001FB38 1 \U0001FB34 damage. ", "damage_dealt"), - seg("[█████████░]", "victory"), seg(" [ 6/7]", ""), + seg("[", ""), seg("█████████", "hp_bar_high"), seg("░", "hp_bar_empty"), seg("]", ""), seg(" [ 6/7]", ""), seg(" (+18xp acc, +6xp hp)", "xp")), ln(seg("The man misses you.", "miss")), ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""), seg("\U0001FB38 6 \U0001FB34 damage. ", "damage_dealt"), - seg("[░░░░░░░░░░]", "error"), seg(" [ 0/7]", ""), + seg("[", ""), seg("░░░░░░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 0/7]", ""), seg(" (+18xp acc, +6xp hp)", "xp")), ln(seg("You have defeated ", ""), seg("the man", "mob"), seg("!", "victory")), ln(seg("*** You are now level 12 accuracy! ***", "level_up")), txt(""), ln(seg("The man", "mob"), seg(" hits you for ", ""), seg("\U0001FB38 5 \U0001FB34 damage. ", "damage_taken"), - seg("[██████░░░░]", "warning"), seg(" [22/40]", "")), + seg("[", ""), seg("██████", "hp_bar_med"), seg("░░░░", "hp_bar_empty"), seg("]", ""), seg(" [22/40]", "")), ln(seg("Oh dear, you are dead!", "death")), txt(""), txt(""), @@ -268,6 +340,87 @@ func buildPreview() []colorSection { }, }) + // --- Constants (player-invisible colors) ---------------------------- + sections = append(sections, colorSection{ + Title: "Constants (admin-only)", + Lines: []colorLine{ + txt(""), + ln(seg("HP bar high ", ""), seg("[", ""), seg("██████", "hp_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("HP bar med ", ""), seg("[", ""), seg("████", "hp_bar_med"), seg("░░░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("HP bar low ", ""), seg("[", ""), seg("██", "hp_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("HP bar empty", ""), seg("[", ""), seg("░░░░░░░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("Task bar high", ""), seg("[", ""), seg("██████", "task_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("Task bar med ", ""), seg("[", ""), seg("████", "task_bar_med"), seg("░░░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("Task bar low ", ""), seg("[", ""), seg("██", "task_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", "")), + ln(seg("BAT ", ""), seg("[", ""), seg("██████", "battery_bar"), seg("░░░░░░░░░░░░░░░░░░░░", ""), seg("] corbat", "")), + ln(seg("ENG ", ""), seg("[", ""), seg("█████████████", "run_energy_bar"), seg("░░░░░░░░░░░░", "hp_bar_empty"), seg("]", "")), + txt(""), + // Constants composed with player colors, as they appear in real + // combat output. Lets the admin see a hp_bar_med / hp_bar_low + // change reflected immediately in a realistic damage line. + ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""), + seg("\U0001FB38 5 \U0001FB34 damage. ", "damage_dealt"), + seg("[", ""), seg("█████", "hp_bar_med"), seg("░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 5/7]", "")), + ln(seg("The man", "mob"), seg(" hits you for ", ""), + seg("\U0001FB38 8 \U0001FB34 damage. ", "damage_taken"), + seg("[", ""), seg("██", "hp_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 3/40]", "")), + ln(seg("Task advance: ", ""), + seg("You advance the work on ", ""), + seg("the ore seam", "mob"), seg(". ", ""), + seg("[", ""), seg("██████", "task_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", ""), seg(" 60%", "")), + txt(""), + ln(seg("Skill levels: ", ""), + seg("99", "skill_lvl_max"), seg(" ", ""), + seg("85", "skill_lvl_high"), seg(" ", ""), + seg("63", "skill_lvl_mid"), seg(" ", ""), + seg("42", "skill_lvl_low"), seg(" ", ""), + seg("12", "skill_lvl_base")), + txt(""), + ln(seg("Level diff: ", ""), + seg("(level 10)", "level_diff_equal"), seg(" vs you 10", ""), + seg(" (level 13)", "level_diff_up_small"), seg(" vs you 10", ""), + seg(" (level 20)", "level_diff_up_big")), + ln(seg(" ", ""), + seg("(level 7)", "level_diff_down_small"), seg(" vs you 10", ""), + seg(" (level 3)", "level_diff_down_big"), seg(" vs you 10", "")), + txt(""), + ln(seg("Technology: ", ""), + seg("Buff Helper ", "tech_name_unlocked"), + seg("ON", "tech_status_on"), seg(" ", ""), + seg("Empty Tech ", "tech_name_locked"), + seg("locked", "tech_status_locked")), + ln(seg(" ", ""), + seg("Off Tech ", "tech_name_unlocked"), + seg("off", "tech_status_off")), + ln(seg("Active: ", ""), + seg("[ON] ", "active_tech_on"), + seg("BullsEye ", "active_tech_stat"), seg("[BUFF] ", "active_tech_status"), seg("Strength", "active_tech_stat")), + ln(seg("Max values: ", ""), + seg("Combat Lvl 99 / HP 40 / BAT 100 / ENG 100", "score_max_value")), + txt(""), + ln(seg("Skill table", ""), seg(" (", "table_header_label"), seg("table_header", "table_header_label"), seg(" ", ""), seg("header", "table_header_label"), seg(") ", ""), + seg("Skill ", "table_header_label"), seg("Abbr ", "table_header_detail"), seg("Level ", "table_header_value"), seg("XP ", "table_header_xp"), seg("XP to Next", "table_header_xp")), + ln(seg("Buff table ", ""), + seg("Option ", "table_header_label"), seg("Value ", "table_header_value"), seg("Valid ", "table_header_detail"), seg("Description", "table_header_desc")), + ln(seg("Production: ", ""), + seg("bronze axe ", "production_locked"), seg("Lv 3 ", "production_locked"), seg("(missing materials)", "production_locked")), + txt(""), + ln(seg("Mob inline HP: ", ""), + seg("a scout", "mob"), seg(" [", ""), seg("3", "mob_hp_inline"), seg("/7hp]", "")), + ln(seg("Protected mob: ", ""), + seg("Dragon", "protected_mob"), seg(" ", ""), seg("(protected)", "protected_label")), + ln(seg("God-mode tags: ", ""), + seg("[LOCAL]", "god_tag_label"), seg(" ", ""), + seg("[GLOBAL]", "god_tag_label"), seg(" ", ""), + seg("(HIDDEN)", "god_tag_label")), + ln(seg("APS Datapad: ", ""), + seg("#2001 ", "table_header_detail"), + seg("Admin Tower ", "table_header_label"), + seg("here", "aps_here")), + txt(""), + }, + }) + return sections } @@ -300,31 +453,64 @@ func (s *AdminServer) getColors(w http.ResponseWriter, _ *http.Request) { def[c.Name] = "" } } + + liveConst := make(map[string]string, len(constantCategories)) + for _, c := range constantCategories { + if v, ok := s.cfg.ConstantColors[c.Name]; ok { + liveConst[c.Name] = v + } else { + liveConst[c.Name] = "" + } + } + constDefaults := config.DefaultConstantColors() + defConst := make(map[string]string, len(constantCategories)) + for _, c := range constantCategories { + if v, ok := constDefaults[c.Name]; ok { + defConst[c.Name] = v + } else { + defConst[c.Name] = "" + } + } + writeJSON(w, map[string]any{ - "colors": live, - "default_colors": def, - "categories": colorCategories, - "preview": buildPreview(), + "colors": live, + "default_colors": def, + "categories": colorCategories, + "constant_colors": liveConst, + "default_constant_colors": defConst, + "constant_categories": constantCategories, + "preview": buildPreview(), }) } func (s *AdminServer) saveColors(w http.ResponseWriter, r *http.Request) { var body struct { - Colors map[string]string `json:"colors"` + Colors map[string]string `json:"colors"` + ConstantColors map[string]string `json:"constant_colors"` } if err := readJSON(r, &body); err != nil { writeJSONError(w, "invalid request body", http.StatusBadRequest) return } - // Validate every supplied key/spec against the known config grammar. + // Validate every supplied player-color key/spec. for name, val := range body.Colors { - if !knownColor(name) { + if !knownPlayerColor(name) { writeJSONError(w, "unknown color category: "+name, http.StatusBadRequest) return } - // "off" or empty is explicitly allowed (NoColor). Anything else must - // parse to a non-empty ColorSpec whose only unknown we tolerate is none. + spec := color.Parse(val) + if strings.TrimSpace(val) != "" && val != "off" && spec.Empty() { + writeJSONError(w, "invalid color spec for "+name+": "+val, http.StatusBadRequest) + return + } + } + // Validate every supplied constant-color key/spec. + for name, val := range body.ConstantColors { + if !knownConstantColor(name) { + writeJSONError(w, "unknown constant color category: "+name, http.StatusBadRequest) + return + } spec := color.Parse(val) if strings.TrimSpace(val) != "" && val != "off" && spec.Empty() { writeJSONError(w, "invalid color spec for "+name+": "+val, http.StatusBadRequest) @@ -342,6 +528,16 @@ func (s *AdminServer) saveColors(w http.ResponseWriter, r *http.Request) { } s.cfg.DefaultColors = next + curConst := s.cfg.ConstantColors + nextConst := make(map[string]string, len(curConst)+len(body.ConstantColors)) + for k, v := range curConst { + nextConst[k] = v + } + for k, v := range body.ConstantColors { + nextConst[k] = v + } + s.cfg.ConstantColors = nextConst + if err := s.writeConfigFile(); err != nil { writeJSONError(w, "save failed: "+err.Error(), http.StatusInternalServerError) return @@ -355,6 +551,7 @@ func (s *AdminServer) handleColorsReset(w http.ResponseWriter, r *http.Request) return } s.cfg.DefaultColors = config.DefaultColors() + s.cfg.ConstantColors = config.DefaultConstantColors() if err := s.writeConfigFile(); err != nil { writeJSONError(w, "save failed: "+err.Error(), http.StatusInternalServerError) return @@ -368,7 +565,15 @@ func (s *AdminServer) handleColorsReset(w http.ResponseWriter, r *http.Request) live[c.Name] = "" } } - writeJSON(w, map[string]any{"ok": true, "colors": live}) + liveConst := make(map[string]string, len(constantCategories)) + for _, c := range constantCategories { + if v, ok := s.cfg.ConstantColors[c.Name]; ok { + liveConst[c.Name] = v + } else { + liveConst[c.Name] = "" + } + } + writeJSON(w, map[string]any{"ok": true, "colors": live, "constant_colors": liveConst}) } // writeConfigFile marshals the entire live *config.Config back to config.yaml, |
