diff --git a/addons/log/log.gd b/addons/log/log.gd new file mode 100644 index 0000000..7a4e5c6 --- /dev/null +++ b/addons/log/log.gd @@ -0,0 +1,484 @@ +@tool +extends Object +class_name Log + +## helpers #################################### + +static func assoc(opts: Dictionary, key: String, val): + var _opts = opts.duplicate(true) + _opts[key] = val + return _opts + +## config #################################### + +static var config = { + max_array_size=20, + dictionary_skip_keys=["layer_0/tile_data"], + } + +static func get_max_array_size(): + return Log.config.get("max_array_size", 20) + +static func get_dictionary_skip_keys(): + return Log.config.get("dictionary_skip_keys", []) + +static func set_color_scheme(scheme): + Log.config["color_scheme"] = scheme + +static func get_config_color_scheme(): + return Log.config.get("color_scheme", {}) + +## colors ########################################################################### + +# terminal safe colors: +# - black +# - red +# - green +# - yellow +# - blue +# - magenta +# - pink +# - purple +# - cyan +# - white +# - orange +# - gray + +static var COLORS_TERMINAL_SAFE = { + "SRC": "cyan", + "ADDONS": "red", + "TEST": "green", + ",": "red", + "(": "red", + ")": "red", + "[": "red", + "]": "red", + "{": "red", + "}": "red", + "&": "orange", + "^": "orange", + "dict_key": "magenta", + "vector_value": "green", + "class_name": "magenta", + TYPE_NIL: "pink", + TYPE_BOOL: "pink", + TYPE_INT: "green", + TYPE_FLOAT: "green", + TYPE_STRING: "pink", + TYPE_VECTOR2: "green", + TYPE_VECTOR2I: "green", + TYPE_RECT2: "green", + TYPE_RECT2I: "green", + TYPE_VECTOR3: "green", + TYPE_VECTOR3I: "green", + TYPE_TRANSFORM2D: "pink", + TYPE_VECTOR4: "green", + TYPE_VECTOR4I: "green", + TYPE_PLANE: "pink", + TYPE_QUATERNION: "pink", + TYPE_AABB: "pink", + TYPE_BASIS: "pink", + TYPE_TRANSFORM3D: "pink", + TYPE_PROJECTION: "pink", + TYPE_COLOR: "pink", + TYPE_STRING_NAME: "pink", + TYPE_NODE_PATH: "pink", + TYPE_RID: "pink", + TYPE_OBJECT: "pink", + TYPE_CALLABLE: "pink", + TYPE_SIGNAL: "pink", + TYPE_DICTIONARY: "pink", + TYPE_ARRAY: "pink", + TYPE_PACKED_BYTE_ARRAY: "pink", + TYPE_PACKED_INT32_ARRAY: "pink", + TYPE_PACKED_INT64_ARRAY: "pink", + TYPE_PACKED_FLOAT32_ARRAY: "pink", + TYPE_PACKED_FLOAT64_ARRAY: "pink", + TYPE_PACKED_STRING_ARRAY: "pink", + TYPE_PACKED_VECTOR2_ARRAY: "pink", + TYPE_PACKED_VECTOR3_ARRAY: "pink", + TYPE_PACKED_COLOR_ARRAY: "pink", + TYPE_MAX: "pink", + } + +static var COLORS_PRETTY_V1 = { + "SRC": "aquamarine", + "ADDONS": "peru", + "TEST": "green_yellow", + ",": "crimson", + "(": "crimson", + ")": "crimson", + "[": "crimson", + "]": "crimson", + "{": "crimson", + "}": "crimson", + "&": "coral", + "^": "coral", + "dict_key": "cadet_blue", + "vector_value": "cornflower_blue", + "class_name": "cadet_blue", + TYPE_NIL: "coral", + TYPE_BOOL: "pink", + TYPE_INT: "cornflower_blue", + TYPE_FLOAT: "cornflower_blue", + TYPE_STRING: "dark_gray", + TYPE_VECTOR2: "cornflower_blue", + TYPE_VECTOR2I: "cornflower_blue", + TYPE_RECT2: "cornflower_blue", + TYPE_RECT2I: "cornflower_blue", + TYPE_VECTOR3: "cornflower_blue", + TYPE_VECTOR3I: "cornflower_blue", + TYPE_TRANSFORM2D: "pink", + TYPE_VECTOR4: "cornflower_blue", + TYPE_VECTOR4I: "cornflower_blue", + TYPE_PLANE: "pink", + TYPE_QUATERNION: "pink", + TYPE_AABB: "pink", + TYPE_BASIS: "pink", + TYPE_TRANSFORM3D: "pink", + TYPE_PROJECTION: "pink", + TYPE_COLOR: "pink", + TYPE_STRING_NAME: "pink", + TYPE_NODE_PATH: "pink", + TYPE_RID: "pink", + TYPE_OBJECT: "pink", + TYPE_CALLABLE: "pink", + TYPE_SIGNAL: "pink", + TYPE_DICTIONARY: "pink", + TYPE_ARRAY: "pink", + TYPE_PACKED_BYTE_ARRAY: "pink", + TYPE_PACKED_INT32_ARRAY: "pink", + TYPE_PACKED_INT64_ARRAY: "pink", + TYPE_PACKED_FLOAT32_ARRAY: "pink", + TYPE_PACKED_FLOAT64_ARRAY: "pink", + TYPE_PACKED_STRING_ARRAY: "pink", + TYPE_PACKED_VECTOR2_ARRAY: "pink", + TYPE_PACKED_VECTOR3_ARRAY: "pink", + TYPE_PACKED_COLOR_ARRAY: "pink", + TYPE_MAX: "pink", + } + +## set color scheme #################################### + +static func set_colors_termsafe(): + set_color_scheme(Log.COLORS_TERMINAL_SAFE) + +static func set_colors_pretty(): + set_color_scheme(Log.COLORS_PRETTY_V1) + +static func color_scheme(opts={}): + var scheme = opts.get("color_scheme", {}) + # fill in any missing vals with the set scheme, then the term-safe fallbacks + scheme.merge(Log.get_config_color_scheme()) + scheme.merge(Log.COLORS_TERMINAL_SAFE) + return scheme + +static func color_wrap(s, opts={}): + var use_color = opts.get("use_color", true) + # don't rebuild the color scheme every time + var colors = opts.get("built_color_scheme", color_scheme(opts)) + + if use_color: + var color = opts.get("color") + if not color: + var s_type = opts.get("typeof", typeof(s)) + if s_type is String: + # type overwrites + color = colors.get(s_type) + elif s_type is int and s_type == TYPE_STRING: + # specific strings/punctuation + var s_trimmed = s.strip_edges() + if s_trimmed in colors: + color = colors.get(s_trimmed) + else: + # fallback string color + color = colors.get(s_type) + else: + # all other types + color = colors.get(s_type) + + if color == null: + print("Log.gd could not determine color for object: %s type: (%s)" % [str(s), typeof(s)]) + + return "[color=%s]%s[/color]" % [color, s] + else: + return s + +## overwrites ########################################################################### + +static var log_overwrites = { + "Vector2": func(msg, opts): + if opts.get("use_color", true): + return '%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s)' % [msg.x, msg.y], + } + +static func register_overwrite(key, handler): + # TODO warning on key exists? + # support multiple handlers? + # return success/fail? + # validate the key/handler somehow? + log_overwrites[key] = handler + +## to_pretty ########################################################################### + +# returns the passed object as a decorated string +static func to_pretty(msg, opts={}): + var newlines = opts.get("newlines", false) + var use_color = opts.get("use_color", true) + var indent_level = opts.get("indent_level", 0) + if not "indent_level" in opts: + opts["indent_level"] = indent_level + + var color_scheme = opts.get("built_color_scheme", color_scheme(opts)) + if not "built_color_scheme" in opts: + opts["built_color_scheme"] = color_scheme + + if not is_instance_valid(msg) and typeof(msg) == TYPE_OBJECT: + return str("invalid instance: ", msg) + + if msg == null: + return Log.color_wrap(msg, opts) + + if msg is Object and msg.get_class() in log_overwrites: + return log_overwrites.get(msg.get_class()).call(msg, opts) + elif typeof(msg) in log_overwrites: + return log_overwrites.get(typeof(msg)).call(msg, opts) + + # objects + if msg is Object and msg.has_method("to_pretty"): + return Log.to_pretty(msg.to_pretty(), opts) + if msg is Object and msg.has_method("data"): + return Log.to_pretty(msg.data(), opts) + if msg is Object and msg.has_method("to_printable"): + return Log.to_pretty(msg.to_printable(), opts) + + # arrays + if msg is Array or msg is PackedStringArray: + if len(msg) > Log.get_max_array_size(): + pr("[DEBUG]: truncating large array. total:", len(msg)) + msg = msg.slice(0, Log.get_max_array_size() - 1) + if newlines: + msg.append("...") + + var tmp = Log.color_wrap("[ ", opts) + var last = len(msg) - 1 + for i in range(len(msg)): + if newlines and last > 1: + tmp += "\n\t" + tmp += Log.to_pretty(msg[i], + # duplicate here to prevent indenting-per-msg + # e.g. when printing an array of dictionaries + opts.duplicate(true)) + if i != last: + tmp += Log.color_wrap(", ", opts) + tmp += Log.color_wrap(" ]", opts) + return tmp + + # dictionary + elif msg is Dictionary: + var tmp = Log.color_wrap("{ ", opts) + var ct = len(msg) + var last + if len(msg) > 0: + last = msg.keys()[-1] + for k in msg.keys(): + var val + if k in Log.get_dictionary_skip_keys(): + val = "..." + else: + opts.indent_level += 1 + val = Log.to_pretty(msg[k], opts) + if newlines and ct > 1: + tmp += "\n\t" \ + + range(indent_level)\ + .map(func(_i): return "\t")\ + .reduce(func(a, b): return str(a, b), "") + if use_color: + var key = Log.color_wrap('"%s"' % k, Log.assoc(opts, "typeof", "dict_key")) + tmp += "%s: %s" % [key, val] + else: + tmp += '"%s": %s' % [k, val] + if last and str(k) != str(last): + tmp += Log.color_wrap(", ", opts) + tmp += Log.color_wrap(" }", opts) + return tmp + + # strings + elif msg is String: + if msg == "": + return '""' + if "[color=" in msg and "[/color]" in msg: + # assumes the string is already colorized + # NOT PERFECT! could use a regex for something more robust + return msg + return Log.color_wrap(msg, opts) + elif msg is StringName: + return str(Log.color_wrap("&", opts), '"%s"' % msg) + elif msg is NodePath: + return str(Log.color_wrap("^", opts), '"%s"' % msg) + + # vectors + elif msg is Vector2 or msg is Vector2i: + return log_overwrites.get("Vector2").call(msg, opts) + + elif msg is Vector3 or msg is Vector3i: + if use_color: + return '%s%s%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s,%s)' % [msg.x, msg.y, msg.z] + elif msg is Vector4 or msg is Vector4i: + if use_color: + return '%s%s%s%s%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.w, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s,%s,%s)' % [msg.x, msg.y, msg.z, msg.w] + + # packed scene + elif msg is PackedScene: + if msg.resource_path != "": + return str(Log.color_wrap("PackedScene:", opts), '%s' % msg.resource_path.get_file()) + elif msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + else: + return Log.color_wrap(msg, opts) + + # resource + elif msg is Resource: + if msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + elif msg.resource_path != "": + return str(Log.color_wrap("Resource:", opts), '%s' % msg.resource_path.get_file()) + else: + return Log.color_wrap(msg, opts) + + # refcounted + elif msg is RefCounted: + if msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + else: + return Log.color_wrap(msg.get_class(), Log.assoc(opts, "typeof", "class_name")) + + # fallback to primitive-type lookup + else: + return Log.color_wrap(msg, opts) + +## to_printable ########################################################################### + +static func log_prefix(stack): + if len(stack) > 1: + var call_site = stack[1] + var basename = call_site["source"].get_file().get_basename() + var line_num = str(call_site.get("line", 0)) + if call_site["source"].match("*/test/*"): + return "{" + basename + ":" + line_num + "}: " + elif call_site["source"].match("*/addons/*"): + return "<" + basename + ":" + line_num + ">: " + else: + return "[" + basename + ":" + line_num + "]: " + +static func to_printable(msgs, opts={}): + var stack = opts.get("stack", []) + var pretty = opts.get("pretty", true) + var newlines = opts.get("newlines", false) + var m = "" + if len(stack) > 0: + var prefix = Log.log_prefix(stack) + var prefix_type + if prefix != null and prefix[0] == "[": + prefix_type = "SRC" + elif prefix != null and prefix[0] == "{": + prefix_type = "TEST" + elif prefix != null and prefix[0] == "<": + prefix_type = "ADDONS" + if pretty: + m += Log.color_wrap(prefix, Log.assoc(opts, "typeof", prefix_type)) + else: + m += prefix + for msg in msgs: + # add a space between msgs + if pretty: + m += "%s " % Log.to_pretty(msg, opts) + else: + m += "%s " % str(msg) + return m.trim_suffix(" ") + +## public print fns ########################################################################### + +static func is_not_default(v): + return not v is String or (v is String and v != "ZZZDEF") + +static func pr(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func info(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func log(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func prn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true}) + print_rich(m) + +static func warn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=yellow][WARN][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_warning(m) + +static func err(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=red][ERR][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_error(m) + +static func error(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=red][ERR][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_error(m) diff --git a/addons/log/plugin.cfg b/addons/log/plugin.cfg new file mode 100644 index 0000000..6e89483 --- /dev/null +++ b/addons/log/plugin.cfg @@ -0,0 +1,14 @@ +[plugin] + +name="Log.gd" +description="A pretty-printing debug logger. + +Log.pr(\"some str\", some_object) + +- Colorizes printed data based on datatype +- Handles nested data structures (Arrays and Dictionaries) +- Prefixes logs with the callsite's source file +- Opt-in to pretty printing via duck-typing (implement a `to_printable()` method on the object)" +author="Russell Matney" +version="v0.0.5" +script="plugin.gd" diff --git a/addons/log/plugin.gd b/addons/log/plugin.gd new file mode 100644 index 0000000..15356b8 --- /dev/null +++ b/addons/log/plugin.gd @@ -0,0 +1,3 @@ +@tool +extends EditorPlugin +