From 83e5214077939cc388684a14cd65123c04884b56 Mon Sep 17 00:00:00 2001 From: su Date: Fri, 10 Jul 2026 09:53:04 +0100 Subject: [PATCH] Overhaul color-schemes for v5X Obsoletes #1599. Cuts out an overwhelming majority of the old script in favour of dfhack-native implementations and a simpler interface. Uses Milo Christiansen's implementation of parsing the color files, because we think it's more elegant. Removes the in-file help text in favour of the .rst documentation. Removes registration as a concept. Stores the default scheme in prefs/color.txt instead of a dedicated file. Anything that uses this script expecting it to work the same as before will break. But then, v5x broke it anyway, so i think that's fine. Only really works with the ASCII interface. We haven't written a changelog entry yet. If this looks good to you [barring a changelog entry, and other changes], we'll move on to season-palette. --- color-schemes.lua | 979 ++++++++--------------------------------- docs/color-schemes.rst | 74 ++-- 2 files changed, 229 insertions(+), 824 deletions(-) diff --git a/color-schemes.lua b/color-schemes.lua index 7343bb6b34..d3faffd30c 100644 --- a/color-schemes.lua +++ b/color-schemes.lua @@ -1,35 +1,7 @@ -- Color schemes manager. ---[====[ - -color-schemes -============= -A script to manage color schemes. - -Current features are : - * :Registration: - * :Loading: - * :Listing: - * :Default: Setting/Loading - -For detailed information and usage, type ``color-schemes`` in console. - -Loaded as a module, this script will export those methods : - * register(path, force) : Register colors schemes by path (file or directory), relative to DF main directory - * load(name) : Load a registered color scheme by name - * list() : Return a list of registered color schemes - * set_default(name) : Set the default color scheme - * load_default() : Load the default color scheme - -For more information about arguments and return values, see ``hack/scripts/color-schemes.lua``. - -Related scripts: - * `gui/color-schemes` is the in-game GUI for this script. - * `season-palette` swaps color schemes when the season changes. - -]====] --[[ -Copyright (c) 2020 Nicolas Ayala `https://github.com/nicolasayala` +Copyright (c) 2016, 2020 Milo Christiansen, Nicolas Ayala `https://github.com/nicolasayala`, with modifications (c) 2026 by Susan et al. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages @@ -50,849 +22,268 @@ freely, subject to the following restrictions: --@ module = true ---+================+ ---| IMPORTS | ---+================+ - -local json = require "json" - ---+================+ ---| GLOBALS | ---+================+ - --- `color-schemes` version -VERSION = "0.1.0" --- Config file (default color-scheme) -CONFIG_FILE = "dfhack-config/color-scheme.json" --- Table of registered color schemes (see Scheme class for template) -_registered_schemes = _registered_schemes or {} --- The current loaded color scheme instance (loaded_scheme.loaded = true) -_loaded_scheme = _loaded_scheme or nil - ---+================+ ---| LOGGING | ---+================+ - --- Log levels -local SUCCESS = 1 -local INFO = 2 -local WARN = 3 -local ERR = 4 --- Log level threshold -local verbosity = 4 - -local log_colors = {COLOR_LIGHTGREEN, COLOR_LIGHTBLUE, COLOR_YELLOW, COLOR_LIGHTRED} -local function log(msg, level) - level = level or 1 - if verbosity - level >= 0 then - dfhack.color(log_colors[level]) - print("color-schemes : " .. msg) - dfhack.color() - end -end - ---+==================+ ---| FILESYSTEM | ---+==================+ +local INITIAL_DEFAULT_COLOR_SCHEME_LOCATION = dfhack.getDFPath() .. "/data/init/colors.txt" +local DEFAULT_COLOR_SCHEME_LOCATION = dfhack.getDFPath() .. "/prefs/colors.txt" ---[[ - Define filesystem methods working with DF's root directory -]]-- -local ROOT = dfhack.getDFPath() .. "/" +--use the new api, if we have it +local CONFIG_PATH = (dfhack.getConfigPath ~= nil) and dfhack.getConfigPath() or (dfhack.getDFPath() .. "/dfhack-config/") -local function open(path, mode) - return io.open(ROOT .. path, mode) -end +--- Directory to search for color schemes in. +COLOR_SCHEME_DIR = CONFIG_PATH .. "color-schemes/" -local function exists(path) - return dfhack.filesystem.exists(ROOT .. path) -end +--- The location of the initial color scheme; prefs/color.txt if it exists, or the dwarf fortress default if not. +local function initial_color_scheme_location() + if dfhack.filesystem.isfile(DEFAULT_COLOR_SCHEME_LOCATION) then + return DEFAULT_COLOR_SCHEME_LOCATION + else + -- fallback to dwarf fortress default + return INITIAL_DEFAULT_COLOR_SCHEME_LOCATION + end +end + +--- The location of the currently active color scheme. read only; to set the active color scheme, use load_color_scheme_from_path. +current_color_scheme_location = current_color_scheme_location or initial_color_scheme_location() + +local SCHEME_COLORS = { + ["BLACK"] = 0, + ["BLUE"] = 1, + ["GREEN"] = 2, + ["CYAN"] = 3, + ["RED"] = 4, + ["MAGENTA"] = 5, + ["BROWN"] = 6, + ["LGRAY"] = 7, + ["DGRAY"] = 8, + ["LBLUE"] = 9, + ["LGREEN"] = 10, + ["LCYAN"] = 11, + ["LRED"] = 12, + ["LMAGENTA"] = 13, + ["YELLOW"] = 14, + ["WHITE"] = 15, +} -local function get_mtime(path) - return dfhack.filesystem.mtime(ROOT .. path) -end +local SCHEME_CHANNELS = { + ["R"] = 0, + ["G"] = 1, + ["B"] = 2, +} -local function listdir(path) - local paths = dfhack.filesystem.listdir_recursive(ROOT .. path) - for _,v in ipairs(paths) do - if string.startswith(v.path, ROOT) then - v.path = string.sub(v.path, #ROOT + 1) - end +--- Get a file handle, looking in COLOR_SCHEME_DIR first, and appending .txt if needed +local function get_file_from_path(path, suppress_errors) + if path:endswith(".txt") == false then + -- color schemes always have the .txt extension, so append it if it's not there already. + path = path .. ".txt" end - return paths -end -local function isdir(path) - return dfhack.filesystem.isdir(ROOT .. path) -end - -local function read_file(path) - if not exists(path) then - return nil, path .. " does not exist" - end - local file = open(path, "rb") - if not file then - return nil, "Could not open " .. path + local file, _ = io.open(COLOR_SCHEME_DIR .. path, "rb") + if file ~= nil then + return file end - local data = file:read("*a") - file:close() - return data -end - -local function decode_file(path, ...) - return json.decode_file(ROOT .. path, ...) -end - -local function encode_file(data, path, ...) - json.encode_file(data, ROOT .. path, ...) -end - -local function sanitize_path(path) - path = string.gsub(path, "/+", "/") - path = string.gsub(path, "^/", "") - return path -end ---+==========================+ ---| COLOR SCHEME PARSING | ---+==========================+ - --- Returns the table flipped (keys and values flipped) -local function flip(table) - local T = {} - for k,v in pairs(table) do - T[v] = k + local file, err = io.open(path, "rb") + if file ~= nil then + return file end - return T -end - -local COLORS = { - "BLACK", "BLUE", "GREEN", "CYAN", "RED", "MAGENTA", - "BROWN", "LGRAY", "DGRAY", "LBLUE", "LGREEN", - "LCYAN", "LRED", "LMAGENTA", "YELLOW", "WHITE" -} - -local CHANNELS = {"R", "G", "B"} ---[[ - Parse colors' values from string with format `[_:]...` - in `COLORS`, in `CHANNELS`, in range [0,255] - Return - valid : Parsing was successful - values : - { - "" = { - "R" = , - "G" = , - "B" = - } - ... - } - ( = -1 when unspecified) -]] -local function parse_color_scheme(string) - local values = {} - local total = 0 - local colors = flip(COLORS) - local channels = flip(CHANNELS) - -- Initialize values table - for color,_ in pairs(colors) do - values[color] = {R = -1, G = -1, B = -1} - end - -- Parse color values - for color, channel, value in string.gmatch(string, "%[([A-Z]+)_([RGB]):([0-9]+)%]") do - value = tonumber(value) - if colors[color] and channels[channel] and value ~= nil and values[color][channel] == -1 then - values[color][channel] = math.max(0, math.min(255, value)) -- Clamp value in range [0,255] - total = total + 1 -- Keep count of parsed colors channels - end + if suppress_errors == false then + dfhack.printerr("color-schemes: could not load " .. err) end - local valid = (total == 48) -- #COLORS * #CHANNELS = 16 * 3 = 48 - return valid, values -end - ---+==================+ ---| SCHEME CLASS | ---+==================+ - ---[[ - Color scheme class, used to store colors' values, file path, modification time, loaded state - template : - { - name : - file : Relative to DF main directory - mtime : File modification time, used for reloading - valid : Specifies if that color scheme is valid (all values are defined) - values: Table of colors' values (see `parse_color_scheme` for template) - } -]] -Scheme = defclass(Scheme) -Scheme.ATTRS { - name = "", - file = "", - mtime = 0, - valid = false, - values = DEFAULT_NIL -} --- Create a color scheme from file -function Scheme.new(file) - file = sanitize_path(file) - -- Parse name from file path, //. (Replace `_` and `-` with spaces) - local name = string.gsub(string.match(file, "([^/]+)%.[^%.]*$"), "[_-]", " ") - -- Read file, get modification time and parse color values - local data, err = read_file(file) - if not data then qerror(err) end - local mtime = get_mtime(file) - local valid, values = parse_color_scheme(data) - return Scheme { - name = name, - file = file, - mtime = mtime, - valid = valid, - values = values - } + return nil end --- Load a color scheme -function Scheme:load() - for c,color in ipairs(COLORS) do - for l,channel in ipairs(CHANNELS) do - -- Use `math.max` for -1 values (not parsed correctly) - local value = math.max(0, self.values[color][channel]) / 255 - df.global.enabler.ccolor[c - 1][l - 1] = value - end +--- Parse the file at the specified path to a color scheme. returns nil on errors. +local function parse_color_scheme(path, quiet, suppress_errors) + local file = get_file_from_path(path, suppress_errors) + if file == nil then + return nil end - df.global.gps.force_full_display_count = 1 -end - ---+===================+ ---| SCRIPT CONFIG | ---+===================+ - -local config = nil -local function get_config() - if not exists(CONFIG_FILE) then - return {} - end - local success, config = pcall(decode_file, CONFIG_FILE) - if not success then qerror(string.format("Unable to parse config file `%s`", CONFIG_FILE)) end - return config -end + -- NOTE: why is this "*a" and not "a"? + local contents = file:read("*a") + file:close() -local function config_set(params) - for param, value in pairs(params) do - config[param] = value + -- initalise result + local result = {} + for color, index in pairs(SCHEME_COLORS) do + result[color] = {R = -1, G = -1, B = -1} end - encode_file(config, CONFIG_FILE) -end - -config = get_config() ---+========================+ ---| LOCAL CORE METHODS | ---+========================+ + local count = 0 ---[[ - Register color scheme from file (relative to DF main directory) - `force` argument forces registering invalid or incomplete color schemes - Return color scheme instance if success, nil otherwise -]] -local function register_file(file, force) - local new = Scheme.new(file) - local mtime = get_mtime(file) - local status - local log_level = INFO - local suffix = "" - local save = true - -- Check if that color scheme was already registered - local old = _registered_schemes[new.name] - if not old then - -- That color scheme isn't already registered - status = "Registered" - elseif new.file ~= old.file then - -- Duplicate color scheme name - status = "Couldn't register" - suffix = "duplicate name" - log_level = ERR - save = false - elseif old.mtime ~= new.mtime then - -- That color scheme was updated - status = "Updated" - else - -- Color scheme already up to date - status = "Up to date" - save = false - end - - if save then - -- Check color scheme validity - if new.valid then - _registered_schemes[new.name] = new - log_level = INFO - else - suffix = "invalid" - if force then - _registered_schemes[new.name] = new - log_level = WARN - else - status = "Couldn't register" - log_level = ERR + for color, channel, v in string.gmatch(contents, "%[([A-Z]+)_([RGB]):([0-9]+)%]") do + local color_index, channel_index, v = SCHEME_COLORS[color], SCHEME_CHANNELS[channel], tonumber(v) + if color_index == nil or channel_index == nil or v == nil then + -- Parse error. + if suppress_errors == false then + dfhack.printerr("color-schemes: " .. path .. " is not a valid color scheme!") end + return nil end - end - local action = string.format("%s `%s`", status, new.name) - local file = string.format("[%s]", new.file) - log(string.format("%-30s %-40s %s", action, file, suffix), log_level) - return save and new or nil; -end --- Register color schemes in directory via `register_file` -local function register_dir(dir, force) - for _,value in ipairs(listdir(dir)) do - if not value.isdir then - if string.match(value.path, ".txt$") then - register_file(value.path, force) + -- clamp values + if v > 255 then + v = 255 + if quiet ~= true then + dfhack.println("color-schemes: Warning: The " .. channel .. " component for color " .. color .. " is out of range! Adjusting...") end end - end -end - ---+======================+ ---| EXPORTED METHODS | ---+======================+ ---[[ - Register color schemes by path (file or directory) - Registering color schemes by directory is recursive - `force` argument forces registering invalid or incomplete color schemes -]] -function register(path, force) - if not exists(path) then - log("Couldn't register file or directory `" .. path .. "` (does not exist)", ERR) - else - if (isdir(path)) then - register_dir(path, force) - else - register_file(path, force) - end + result[color][channel] = v + count = count + 1 end -end ---[[ - Load a registered color scheme by name - The color scheme must be first registered with the `register` method - Return the color scheme instance -]] -function load(name) - local scheme = _registered_schemes[name] - if not scheme then - log("Unregistered color scheme `" .. name .. "`", ERR) + -- check count + local VALID_COUNT = 48 -- #SCHEME_COLORS * #SCHEME_CHANNELS = 16 * 3 = 48 + if count ~= VALID_COUNT then + if suppress_errors == false then + dfhack.printerr("color-schemes: " .. path .. " is not a valid color scheme! (are there missing entries?)") + end return nil end - if scheme ~= _loaded_scheme then - _loaded_scheme = scheme - scheme:load() - log("Loaded `" .. scheme.name .. "`") - else - log("Color scheme `" .. scheme.name .. "` already loaded !", WARN) - end - return scheme -end - ---[[ - Return the table of registered color schemes - template : - { - { - name : - file : - valid : - loaded : - } - ... - } -]] -function list() - local schemes = {} - for name,scheme in pairs(_registered_schemes) do - table.insert(schemes, { - name = name, - file = scheme.file, - valid = scheme.valid, - loaded = (scheme == _loaded_scheme), - default = (scheme.name == config.default) - }) - end - return schemes -end - ---[[ - Set the default color scheme name, stored in `CONFIG_FILE`, persistent between restarts. -]] -function set_default(name) - if name then - log(string.format("Default set to `%s`", name), SUCCESS) - config_set({default = name}) - end -end ---[[ - Load the default color scheme, however it must be registered first like any other color scheme. - (see `dfhack*.init` files to register color schemes on start) -]] -function load_default() - local default = config.default - if not default then - log("No default color scheme", ERR) - return nil - end - return load(default) + return result end -if moduleMode then return _ENV end - ---+==================+ ---| ARGS PARSING | ---+==================+ +--- List color schemes found in the COLOR_SCHEME_DIR directory. +function available_color_schemes() + local list = dfhack.filesystem.listdir_recursive(COLOR_SCHEME_DIR) or {} ---[[ - Print a formated table with colors - <-----spacing_0----> <-----spacing_1-----> ... - | |... - ====================|=====================|... - .column_0 |.column_1 |... - .column_0 |.column_1 |... - - Args - T :
- on_print : Function called before entry is printed - ... : Format ":" (column name and spacing) -]] -local function printSchemes(T, on_print, ...) - local format = {...} - local columns = {} - local spacings = {} - local entry_format = "" - local sep = "|" - local bar = "" - -- Parse columns' names and build entry format - for i,c in ipairs(format) do - if i == #format then sep = "" end - local name, spacing = string.match(c, "(%a+):(%d+)") - entry_format = entry_format .. "%-" .. spacing .. "s" .. sep - bar = bar .. string.rep("=", tonumber(spacing)) .. sep - table.insert(columns, name) - table.insert(spacings, tonumber(spacing)) - end - print(string.format(entry_format, table.unpack(columns))) - print(bar) - -- Build entry with specified columns - for name,item in pairs(T) do - local entry = {} - for i,c in ipairs(columns) do - -- Clamp string fields to match spacing - if type(item[c]) == "string" and #item[c] > spacings[i] then - table.insert(entry, string.sub(item[c], 0, spacings[i]-3) .. "...") - else - table.insert(entry, item[c]) + local result = {} + for _, entry in ipairs(list) do + if entry.isdir == false then + if string.match (entry.path, ".txt") then + -- parsing every file to see if it's valid is wasteful, but it's either this or registration... + local scheme = parse_color_scheme(entry.path, true, true) + if scheme ~= nil then + -- remove the COLOR_SCHEME_DIR prefix + local stripped_path = entry.path:sub(#COLOR_SCHEME_DIR + 1) + -- remove the .txt extension + stripped_path = stripped_path:sub(0, -5) -- lua indexing is wacky... + dfhack.println(entry.path, COLOR_SCHEME_DIR) + result[#result + 1] = stripped_path + end end end - if on_print then on_print(item) end - print(string.format(entry_format, table.unpack(entry))) - dfhack.color() end -end -local function map(f, T, i, j) - local M = {} - for k = (i or 1),(j or #T) do - M[k] = f(k, T[k]) - end - return M -end - -local function extract(T, i, j) - return {table.unpack(T, i, j)} -end - -local function find(v, T) - for key,value in ipairs(T) do - if v == value then return key, value end - end - return nil + return result end -local function concat(T, f, sep, i, j) - return table.concat(map(function(k, v) return f(v) end, T, i, j), sep) -end - -local function get(self, key) - if type(self[key]) == "function" then - return self[key](self) - else - return self[key] - end -end - -local class = function(class, parent, attrs) - local class = defclass(class, parent) - local ATTRS = {} - for attr,default in pairs(attrs or {}) do - local internal = string.format("_%s", attr) - ATTRS[internal] = default - class[attr] = function(self, v) self[internal] = v return self end +--- Set the active color scheme. +local function load_color_scheme(scheme) + for color, color_index in pairs(SCHEME_COLORS) do + for channel, channel_index in pairs(SCHEME_CHANNELS) do + local value = scheme[color][channel] + df.global.gps.uccolor[color_index][channel_index] = value + end end - class.ATTRS(ATTRS) - return class -end - ---+=====================+ ---| PARSING CLASSES | ---+=====================+ - ---[[ PItem ]]-- - -PItem = class(PItem, nil, - { - name = DEFAULT_NIL, -- string - description = "", -- string - action = DEFAULT_NIL, -- boolean func(self, result, target, args) - } -) - -local actions = { - store = function(self, result, target, args) result[target] = args end, - store_true = function(self, result, target, args) result[target] = true end, - store_false = function(self, result, target, args) result[target] = false end, - count = function(self, result, target, args) result[target] = 1 or result[target] + 1 end -} - -function PItem:init(args) - local class = getmetatable(self) - class.__tostring = class.__tostring or class.super.__tostring -end - -function PItem:error(format, ...) - return qerror(string.format(format, ...)) -end - -function PItem:__tostring() - return string.format("%-20s %s", self._name, self._description) -end - ---[[ Argument ]]-- - -Argument = class(Argument, PItem, - { - action = actions.store, -- boolean func(self, result, target, args) - args = 1, -- number - choices = DEFAULT_NIL, -- table - convert = DEFAULT_NIL, -- function(arg) - } -) - -function Argument:usage() - return string.rep(string.format("<%s>", self._name), self._args, " ") + -- NOTE i'm not sure why this is here, it doesn't seem to be required. possibly a leftover from when ccolor was used? + df.global.gps.force_full_display_count = 1 end -function Argument:consume(args, result) - local A = {} - local consumed = 0 - for i = 1,self._args do - local arg = args[i] - if not arg then - self:error("argument `%s` requires %d argument(s)", self._name, self._args) - end - if self._choices then - if not find(arg, self._choices) then - local C = concat(self._choices, curry(string.format, "`%s`"), ", ") - self:error("argument `%s` must be one of %s", self._name, C) - end +--- Load a color scheme from the specified path. +function load_color_scheme_from_path(path, quiet) + local scheme = parse_color_scheme(path, quiet, false) + if scheme ~= nil then + load_color_scheme(scheme) + current_color_scheme_location = path + if quiet ~= true then + dfhack.println("color-schemes: loaded " .. path .. ".") end - consumed = consumed + 1 - if self._convert then - local res = self._convert(arg) - if not res then self:error("argument `%s` could not be converted", arg) end - table.insert(A, res) - else - table.insert(A, arg) - end - end - local sub_args = extract(args, consumed + 1) - if self:_action(result, self._target, (consumed == 1) and A[1] or A) then - return sub_args, true end - return sub_args end -function Argument:name(name) - self._name = name - self._target = name - return self +--- Load the default color scheme from prefs/color.txt if it exists, or the dwarf fortress default if not. +function load_default_color_scheme(quiet) + load_color_scheme_from_path(initial_color_scheme_location(), quiet) end ---[[ Option ]]-- - -Option = class(Option, Argument) - -function Option:name(name) - self._aliases = self._aliases or {} - for _,alias in ipairs(string.split(name, " ")) do - local short = alias:match("^-%a$") - local long = alias:match("^%-%-%a+$") - if short then self._short = short end - self._target = string.gsub(short or long, "%-", "") - table.insert(self._aliases, short or long) +--- Overwrite the default color scheme with the specified file. Assumes that this is a valid scheme. +function set_default_color_scheme(path, quiet) + local scheme_file, err = io.open(path, "rb") + if scheme_file == nil then + dfhack.printerr("color-schemes: could not read " .. path .. ": " .. err) + return end - self._name = name - return self -end -function Option:usage() - if self._args == 0 then - return string.format("[%s]", self._short) - else - return string.format("[%s %s]", self._short, Argument.usage(self)) - end -end + local scheme_file_contents = scheme_file:read("a") + scheme_file:close() -function Option:__tostring() - if self._args == 0 then - return string.format("%-20s %s", table.concat(self._aliases, ", "), self._description) - else - local arg = string.format("<%s>", self._target) - local args = string.rep(arg, self._args, " ") - local lines = map(function(k,v) return string.format("%s %s", v, args) end, self._names) - local width = 0 - for _,l in ipairs(lines) do width = math.max(width, #l) end - local help = concat(lines, curry(string.format, "%" .. width .. "s"), ",\n ") - return string.format("%s\n %20s %s", help, "", self._description) + local default_file, err = io.open(DEFAULT_COLOR_SCHEME_LOCATION, "w") + if default_file == nil then + dfhack.printerr("color-schemes: could not open " .. DEFAULT_COLOR_SCHEME_LOCATION .. ": " .. err) + return end -end ---[[ Parser ]]-- - -Parser = class(Parser, PItem, - { - action = actions.store, -- boolean func(self, result, target, args) - summary = DEFAULT_NIL, -- string - notes = DEFAULT_NIL, -- string - req_command = true, -- boolean - } -) - -Parser.help_items = { - {key = "_name", format = "%s\n"}, - {key = "_description", format = "\n%s\n"}, - {key = "usage", format = "\nUsage: %s\n"}, - {key = "_arguments", format = "\nArguments:\n%s\n"}, - {key = "_options", format = "\nOptions:\n%s\n"}, - {key = "_commands", format = "\nCommands:\n%s\n"}, - {key = "_notes", format = "\nNotes:\n%s\n"}, -} - -Parser.usage_items = { - {key = "_name", usage = function(value) return value end}, - {key = "_options", usage = function(value) return concat(value, Option.usage, " ") end}, - {key = "_arguments", usage = function(value) return concat(value, Argument.usage, " ") end}, - {key = "_commands", usage = function(value) return "..." end}, -} + default_file:write(scheme_file_contents) + default_file:close() -function Parser:command() - local command = Parser() - self._commands = self._commands or {} - table.insert(self._commands, command) - return command + if quiet ~= true then + dfhack.println("color-schemes: set default scheme to" .. path .. ".") + end end -function Parser:argument() - local argument = Argument() - self._arguments = self._arguments or {} - table.insert(self._arguments, argument) - return argument +--- Reset the default color scheme to the one that comes with dwarf fortress. +function reset_default_color_scheme() + set_default_color_scheme(INITIAL_DEFAULT_COLOR_SCHEME_LOCATION) end -function Parser:option() - local option = Option() - self._options = self._options or {} - table.insert(self._options, option) - return option +if dfhack_flags.module then + return end -function Parser:flag() - return self - :option() - :args(0) - :action(actions.store_true) -end +local options = {} -function Parser:add_help() - self:flag() - :name("-h --help") - :description("Show this help message and exit") - :action(function() print(self:help()) return true end) - return self -end +local argparse = require('argparse') +local commands = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() options.help = true end}, + {'q', 'quiet', handler=function() options.quiet = true end}, +}) -function Parser:__tostring() - return string.format("%-20s %s", self._name, self._summary or self._description) +if options.help == true then + dfhack.println(dfhack.script_help()) + return end -function Parser:usage() - local parts = {} - for _,item in ipairs(Parser.usage_items) do - local value = self[item.key] - if value then - table.insert(parts, item.usage(value)) - end - end - return table.concat(parts, " ") +if commands[1] == "load" and #commands == 2 then + local path = commands[2] + load_color_scheme_from_path(path, options.quiet) + return end -function Parser:help() - local help_format = "" - local help_args = {} - for _,item in ipairs(Parser.help_items) do - local value = get(self, item.key) - if value then - help_format = help_format .. item.format - local value_str - if type(value) == "table" then - value_str = " " .. concat(value, tostring, "\n ") - elseif type(value) == "string" then - value_str = value - end - table.insert(help_args, value_str) - end +if commands[1] == "list" and #commands == 1 then + local list = available_color_schemes() + for _, entry in ipairs(list) do + dfhack.println(entry) end - return help_format:format(table.unpack(help_args)) + return end -function Parser:parse(args, result) - local result = result or {} - local arg = args[1] - if not arg then - if self._commands and self._req_command then - self:error("a command is required") - elseif self._arguments then - for _,argument in ipairs(self._arguments) do - if not result[argument._name] then - self:error("missing argument `%s`", argument._name) - end - end - end - self:_action(result, self._name) - return result +if commands[1] == "default" then + if commands[2] == "load" and #commands == 2 then + load_default_color_scheme(options.quiet) + return end - -- Options - if arg:sub(1, 1) == "-" and self._options then - local o = arg - for _,option in ipairs(self._options) do - if find(o, option._aliases) then - local sub_args, exit = option:consume(extract(args, 2), result) - if exit then return result end - return self:parse(sub_args, result) - end + if commands[2] == "set" then + if #commands == 2 then + set_default_color_scheme(current_color_scheme_location, options.quiet) + return end - self:error("unknown option `%s`", o) - end - -- Arguments - if self._arguments then - for _,argument in ipairs(self._arguments) do - if not result[argument._name] then - local sub_args, exit = argument:consume(args, result) - if exit then return result end - return self:parse(sub_args, result) - end + if #commands == 3 then + local path = commands[3] + set_default_color_scheme(path, options.quiet) + return end end - -- Commands - local c = arg - if self._commands then - for _,command in ipairs(self._commands) do - if c == command._name then - result.command = command:parse(extract(args, 2)) - return result - end - end - self:error("unknown command `%s`", c) + if commands[2] == "reset" and #commands == 2 then + reset_default_color_scheme(options.quiet) + return end - self:error("too many arguments") end ---[[ Parser Config ]]-- - -local parser = Parser() -parser - :name("color-schemes"):description("Manage color schemes") - :add_help() - :notes(" Type `--help` after a command to get further information") -parser:flag() - :name("-V --version"):description("Show version number and exit") - :action(function() print(VERSION) return true end) -parser:flag() - :name("-q --quiet"):description("Do not write anything to output") - :action(function() verbosity = 0 end) - -local load_cmd = parser:command() -load_cmd - :name("load"):description("Load a registered color scheme") - :add_help() - :action(function(self, result) load(result.name) end) -load_cmd:argument() - :name("name") - :description("Color scheme name") - -local list_cmd = parser:command() -list_cmd - :name("list"):description("List registered color schemes") - :add_help() - :action( - function() - printSchemes( - list(), - function(item) - if not item.valid then return dfhack.color(COLOR_LIGHTRED) - elseif item.loaded then return dfhack.color(COLOR_LIGHTGREEN) end - end, - "name:30", "file:50" - ) - end - ) - -local register_cmd = parser:command() -register_cmd - :name("register"):description("Register color schemes") - :add_help() - :action(function(self, result) register(result.path, result.force) end) -register_cmd:flag() - :name("-f --force"):description("Also register syntaxically incorrect or partial color schemes") -register_cmd:argument() - :name("path"):description("Path to color scheme file or directory") - -local default_cmd = parser:command() -default_cmd - :name("default"):description("Load or set the default color scheme") - :add_help() -default_cmd:command() - :name("load"):description("Load the default color scheme") - :add_help() - :action(curry(load_default)) -default_cmd:command() - :name("set"):description("Set the default color scheme") - :add_help() - :action(function(self, result) set_default(result.name) end) - :argument() - :name("name"):description("Color scheme name") - -local args = {...} -if #args == 0 then print(parser:help()) return _ENV end -local result = parser:parse(args) --- result table isn't used here since logic is inside parsing actions - -return _ENV +dfhack.println(dfhack.script_help()) diff --git a/docs/color-schemes.rst b/docs/color-schemes.rst index 07445b20fd..d447540033 100644 --- a/docs/color-schemes.rst +++ b/docs/color-schemes.rst @@ -2,13 +2,15 @@ color-schemes ============= .. dfhack-tool:: - :summary: Modify the colors used by the DF UI. - :tags: unavailable + :summary: Modify the colors used by the classic ASCII interface. + :tags: unavailable interface This tool allows you to set exactly which shades of colors should be used in the -DF interface color palette. +color palette of the classic DF interface. Unfortunately, this tool can *only* +modify the colours of the classic interface; when using the new interface, only +the color of text can be modified, which is probably not what you want. -To set up the colors, you must first create at least one file with color +To set up color schemes, you must first create at least one file with color definitions inside. These files must be in the same format as :file:`data/init/colors.txt` and contain RGB values for each of the color names. Just copy :file:`colors.txt` and edit the values for your custom color schemes. @@ -21,43 +23,50 @@ If you are interested in alternate color schemes, also see: Usage ----- -``color-schemes register [-f] [-q]`` - Register the directory (relative to the main DF game directory) where your - color scheme files are stored. ``color-schemes list`` - List the color schemes from the registered directories. -``color-schemes default set [-q]`` - Set the named color scheme as the default. This value is stored so you only - have to set it once, even if you start a new adventure/fort. + List available color schemes in :file:`dfhack-config/color-schemes`. +``color-schemes default set [-q]`` + Set the given color scheme as the default. This file is saved as :file:`prefs/colors.txt` + so you only have to set it once, even if you start a new adventure/fort. ``color-schemes default load [-q]`` Load the default color scheme that you previously set with ``default set``. -``color-schemes load [-q]`` - Load the named color scheme. +``color-schemes load [-q]`` + Load the specified color scheme. +``color-schemes default reset [-q]`` + Reset the default color scheme to the original color scheme that comes with the game. Examples -------- -Read your color scheme files from the :file:`colorschemes` directory (a -directory you created and populated with color scheme files) and set the -default to the scheme named ``mydefault``:: +List color scheme files found in the :file:`dfhack-config/color-schemes` directory:: + + color-schemes list + +Load a colour scheme from the location :file:`dfhack-config/color-schemes/foo.txt`:: + + color-schemes load foo + +Load a colour scheme from the location :file:`/home/urist/bar.txt`:: + + color-schemes load /home/urist/bar.txt + +Set the default color scheme to the currently loaded scheme:: + + color-schemes default set + +Set the default color scheme to a scheme located at :file:`dfhack-config/color-schemes/mydefault.txt`, and load it:: - color-schemes register colorschemes color-schemes default set mydefault + color-schemes default load -Read your color scheme files from the :file:`colorschemes` directory (a -directory you created and populated with color scheme files) and load the saved -default. If you have a color scheme that you always want loaded, put these -commands in your :file:`dfhack-config/init/dfhack.init` file:: +Change the default color scheme back to the initial dwarf fortress default:: - color-schemes -q register colorschemes + color-schemes default reset color-schemes default load Options ------- -``-f``, ``--force`` - Register and read color schemes that are incomplete or are syntactically - incorrect. ``-q``, ``--quiet`` Don't print any informational output. @@ -66,8 +75,13 @@ API When loaded as a module, this script will export the following functions: -- ``register(path, force)`` : Register colors schemes by path (file or directory), relative to DF main directory -- ``load(name)`` : Load a registered color scheme by name -- ``list()`` : Return a list of registered color schemes -- ``set_default(name)`` : Set the default color scheme -- ``load_default()`` : Load the default color scheme +- ``load_color_scheme_from_path(path)`` : Load a registered color scheme by path, searching first in ``COLOR_SCHEME_DIR`` +- ``available_color_schemes()`` : Return a list of registered color schemes +- ``set_default_color_scheme(path)`` : Set the default color scheme +- ``load_default_color_scheme()`` : Load the default color scheme +- ``reset_default_color_scheme()`` : Reset the default color scheme to the color scheme that comes with the game + +The following variables will also be made available: + +``COLOR_SCHEME_DIR`` : Shorthand for :file:`dfhack-config/color-schemes/` +``current_color_scheme_location`` : The location of the currently loaded color scheme. You should use ``load_color_scheme_from_path`` to change this - setting it manually won't do anything