diff --git a/src/CTBase.jl b/src/CTBase.jl index 46466231..57690114 100644 --- a/src/CTBase.jl +++ b/src/CTBase.jl @@ -12,14 +12,14 @@ import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # MODULAR ORGANIZATION # ============================================================================ # -# Exceptions module - enhanced error handling system (must load first) -include(joinpath(@__DIR__, "Exceptions", "Exceptions.jl")) -using .Exceptions - -# Core module - fundamental types and utilities +# Core module - fundamental types and utilities (must load first) include(joinpath(@__DIR__, "Core", "Core.jl")) using .Core +# Exceptions module - enhanced error handling system +include(joinpath(@__DIR__, "Exceptions", "Exceptions.jl")) +using .Exceptions + # Unicode module - Unicode character utilities include(joinpath(@__DIR__, "Unicode", "Unicode.jl")) using .Unicode diff --git a/src/Core/Core.jl b/src/Core/Core.jl index 75e37d97..93bc5013 100644 --- a/src/Core/Core.jl +++ b/src/Core/Core.jl @@ -11,6 +11,7 @@ module Core import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES include("types.jl") +include("tags.jl") include("default.jl") # Private utilities @@ -19,6 +20,7 @@ include("macros.jl") # Public utilities include("matrix_utils.jl") +include("display.jl") # Export public API export ctNumber, matrix2vec, to_out_of_place, @ensure diff --git a/src/Core/display.jl b/src/Core/display.jl new file mode 100644 index 00000000..ee17b832 --- /dev/null +++ b/src/Core/display.jl @@ -0,0 +1,131 @@ +""" + _apply_ansi(s, code, io::IO) + +Apply ANSI escape codes to a string if color is enabled in the IO context. + +# Arguments +- `s::AbstractString`: The string to style. +- `code::String`: The ANSI code (e.g., `"2"` for dim, `"1"` for bold, `"1;31"` for red). +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in ANSI escape codes if `get(io, :color, false)` is true, + otherwise the plain string. +""" +_apply_ansi(s, code, io::IO) = get(io, :color, false) ? "\033[$(code)m$(s)\033[0m" : s + +""" +$(TYPEDSIGNATURES) + +Apply dimmed (faint) ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in dim ANSI escape codes if color is enabled. +""" +_dim(s, io::IO) = _apply_ansi(s, "2", io) + +""" +$(TYPEDSIGNATURES) + +Apply bold ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in bold ANSI escape codes if color is enabled. +""" +_bold(s, io::IO) = _apply_ansi(s, "1", io) + +""" +$(TYPEDSIGNATURES) + +Apply red ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in red ANSI escape codes if color is enabled. +""" +_red(s, io::IO) = _apply_ansi(s, "1;31", io) + +""" +$(TYPEDSIGNATURES) + +Apply yellow ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in yellow ANSI escape codes if color is enabled. +""" +_yellow(s, io::IO) = _apply_ansi(s, "33", io) + +""" +$(TYPEDSIGNATURES) + +Apply green ANSI styling to a string. + +# Arguments +- `s::AbstractString`: The string to style. +- `io::IO`: Output stream to check for color support. + +# Returns +- `String`: The string wrapped in green ANSI escape codes if color is enabled. +""" +_green(s, io::IO) = _apply_ansi(s, "32", io) + +""" + get_format_codes(io::IO) -> NamedTuple + +Get ANSI formatting codes based on terminal color support. + +Returns a NamedTuple with formatting codes for consistent display across all show() methods. + +# Fields +- `bold`: Bold text +- `reset`: Reset all formatting +- `name`: Bold blue for names (options, types, etc.) +- `type`: Cyan for types +- `value`: Green for values +- `keyword`: Yellow for keywords/aliases +- `count`: Magenta for counts +- `label`: Gray for labels/descriptions + +# Example +```julia +fmt = get_format_codes(io) +print(io, fmt.name, "option_name", fmt.reset, "::", fmt.type, "Int", fmt.reset) +``` + +# Notes +- Automatically detects color support via `get(io, :color, false)` +- Returns empty strings for all codes if colors are not supported +- Ensures consistent color scheme across the entire package +""" +function get_format_codes(io::IO) + supports_color = get(io, :color, false) + + return ( + # Text formatting + bold=supports_color ? "\033[1m" : "", + reset=supports_color ? "\033[0m" : "", + + # Colors for different semantic elements + name=supports_color ? "\033[1m\033[34m" : "", # Bold blue for names + type=supports_color ? "\033[36m" : "", # Cyan for types + value=supports_color ? "\033[32m" : "", # Green for values + keyword=supports_color ? "\033[33m" : "", # Yellow for keywords/aliases + count=supports_color ? "\033[35m" : "", # Magenta for counts + label=supports_color ? "\033[90m" : "", # Gray for labels + ) +end diff --git a/src/Core/tags.jl b/src/Core/tags.jl new file mode 100644 index 00000000..ff458fd4 --- /dev/null +++ b/src/Core/tags.jl @@ -0,0 +1,20 @@ +""" + AbstractTag + +Abstract type for tag dispatch pattern used to handle extension-dependent implementations. + +This type is used for multiple dispatch in validation functions and other contexts +where behavior depends on loaded extensions (e.g., Enzyme, Zygote, CUDA). + +# Example +```julia +struct MyTag <: AbstractTag end + +function validate_backend(tag::MyTag, backend::Symbol) + # Tag-specific validation logic +end +``` + +See also: Extension-based validation patterns in extension modules +""" +abstract type AbstractTag end diff --git a/src/Exceptions/Exceptions.jl b/src/Exceptions/Exceptions.jl index b14de2d8..a2c003ff 100644 --- a/src/Exceptions/Exceptions.jl +++ b/src/Exceptions/Exceptions.jl @@ -37,6 +37,7 @@ The Exceptions module is organized into thematic files: module Exceptions import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES +using ..Core: Core # Type definitions include("types.jl") diff --git a/src/Exceptions/display.jl b/src/Exceptions/display.jl index 79f8b182..fcfd8e07 100644 --- a/src/Exceptions/display.jl +++ b/src/Exceptions/display.jl @@ -1,92 +1,3 @@ -# Custom display functions for user-friendly error messages - -# ANSI formatting primitives -""" - _apply_ansi(s, code, io::IO) - -Apply ANSI escape codes to a string if color is enabled in the IO context. - -# Arguments -- `s::AbstractString`: The string to style. -- `code::String`: The ANSI code (e.g., "2" for dim, "1" for bold, "1;31" for red). -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in ANSI escape codes if `get(io, :color, false)` is true, - otherwise the plain string. -""" -_apply_ansi(s, code, io::IO) = get(io, :color, false) ? "\033[$(code)m$(s)\033[0m" : s - -""" -$(TYPEDSIGNATURES) - -Apply dimmed (faint) ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in dim ANSI escape codes if color is enabled. -""" -_dim(s, io::IO) = _apply_ansi(s, "2", io) - -""" -$(TYPEDSIGNATURES) - -Apply bold ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in bold ANSI escape codes if color is enabled. -""" -_bold(s, io::IO) = _apply_ansi(s, "1", io) - -""" -$(TYPEDSIGNATURES) - -Apply red ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in red ANSI escape codes if color is enabled. -""" -_red(s, io::IO) = _apply_ansi(s, "1;31", io) - -""" -$(TYPEDSIGNATURES) - -Apply yellow ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in yellow ANSI escape codes if color is enabled. -""" -_yellow(s, io::IO) = _apply_ansi(s, "33", io) - -""" -$(TYPEDSIGNATURES) - -Apply green ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. - -# Returns -- `String`: The string wrapped in green ANSI escape codes if color is enabled. -""" -_green(s, io::IO) = _apply_ansi(s, "32", io) - """ extract_user_frames(st::Vector) @@ -360,16 +271,16 @@ function _print_pipe_field(io, label::String, value, max_len::Int, color::Symbol # Multi-line case: AmbiguousDescription.candidates for (i, v) in enumerate(value) if i == 1 - print(io, _dim("│", io), " ", _bold(rpad(label, max_len), io), " ") + print(io, Core._dim("│", io), " ", Core._bold(rpad(label, max_len), io), " ") _print_colored(io, string(v), color) println(io) else - println(io, _dim("│", io), " ", " "^max_len, " ", string(v)) + println(io, Core._dim("│", io), " ", " "^max_len, " ", string(v)) end end else # Single value - print(io, _dim("│", io), " ", _bold(rpad(label, max_len), io), " ") + print(io, Core._dim("│", io), " ", Core._bold(rpad(label, max_len), io), " ") _print_colored(io, string(value), color) println(io) end @@ -390,9 +301,9 @@ Print colored text based on a color symbol. """ function _print_colored(io, text, color::Symbol) if color == :yellow - print(io, _yellow(text, io)) + print(io, Core._yellow(text, io)) elseif color == :green - print(io, _green(text, io)) + print(io, Core._green(text, io)) else print(io, text) end @@ -413,22 +324,22 @@ function _format_user_friendly_error(io::IO, e::CTException) frame = isempty(user_frames) ? nothing : user_frames[1] type_name = string(nameof(typeof(e))) - print(io, _red(type_name, io)) + print(io, Core._red(type_name, io)) if !isnothing(frame) func_name = string(frame.func) file_name = basename(string(frame.file)) line_num = frame.line - print(io, " ", _dim("→", io), " ", _bold(func_name, io), " ") - print(io, _yellow("$(file_name):$(line_num)", io)) + print(io, " ", Core._dim("→", io), " ", Core._bold(func_name, io), " ") + print(io, Core._yellow("$(file_name):$(line_num)", io)) end println(io) # Blank pipe separator - println(io, _dim("│", io)) + println(io, Core._dim("│", io)) # Message - println(io, _dim("│", io), " ", _bold(e.msg, io)) + println(io, Core._dim("│", io), " ", Core._bold(e.msg, io)) # Build field pairs primary_pairs = _build_primary_pairs(e) @@ -440,7 +351,7 @@ function _format_user_friendly_error(io::IO, e::CTException) max_len = maximum(length(p[1]) for p in all_pairs) # Blank pipe separator - println(io, _dim("│", io)) + println(io, Core._dim("│", io)) # Primary fields for p in primary_pairs @@ -449,7 +360,7 @@ function _format_user_friendly_error(io::IO, e::CTException) # Separator between primary and secondary if !isempty(primary_pairs) && !isempty(secondary_pairs) - println(io, _dim("│", io)) + println(io, Core._dim("│", io)) end # Secondary fields @@ -459,7 +370,7 @@ function _format_user_friendly_error(io::IO, e::CTException) end # Closing visual - return println(io, _dim("└─", io)) + return println(io, Core._dim("└─", io)) end """ diff --git a/test/suite/core/test_core_display.jl b/test/suite/core/test_core_display.jl new file mode 100644 index 00000000..1708f79a --- /dev/null +++ b/test/suite/core/test_core_display.jl @@ -0,0 +1,86 @@ +module TestCoreDisplay + +using Test: Test +import CTBase.Core + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +function test_core_display() + io_plain = IOContext(devnull, :color => false) + io_color = IOContext(devnull, :color => true) + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_apply_ansi" begin + Test.@testset "no color — returns plain string" begin + Test.@test Core._apply_ansi("hello", "1", io_plain) == "hello" + Test.@test Core._apply_ansi("hello", "2", io_plain) == "hello" + Test.@test Core._apply_ansi("hello", "1;31", io_plain) == "hello" + end + + Test.@testset "with color — wraps with escape codes" begin + result = Core._apply_ansi("hello", "42", io_color) + Test.@test startswith(result, "\033[42m") + Test.@test endswith(result, "\033[0m") + Test.@test contains(result, "hello") + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "semantic wrappers" begin + Test.@testset "no color — all return plain string" begin + Test.@test Core._dim("x", io_plain) == "x" + Test.@test Core._bold("x", io_plain) == "x" + Test.@test Core._red("x", io_plain) == "x" + Test.@test Core._yellow("x", io_plain) == "x" + Test.@test Core._green("x", io_plain) == "x" + end + + Test.@testset "with color — all wrap string" begin + for f in (Core._dim, Core._bold, Core._red, Core._yellow, Core._green) + result = f("x", io_color) + Test.@test startswith(result, "\033[") + Test.@test endswith(result, "\033[0m") + Test.@test contains(result, "x") + end + end + + Test.@testset "each wrapper uses distinct ANSI code" begin + results = map(f -> f("x", io_color), (Core._dim, Core._bold, Core._red, Core._yellow, Core._green)) + Test.@test allunique(results) + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "get_format_codes" begin + Test.@testset "no color — all fields are empty strings" begin + fmt = Core.get_format_codes(io_plain) + for field in (:bold, :reset, :name, :type, :value, :keyword, :count, :label) + Test.@test fmt[field] == "" + end + end + + Test.@testset "with color — all fields are non-empty" begin + fmt = Core.get_format_codes(io_color) + for field in (:bold, :reset, :name, :type, :value, :keyword, :count, :label) + Test.@test fmt[field] != "" + end + end + + Test.@testset "returns a NamedTuple with expected fields" begin + fmt = Core.get_format_codes(io_plain) + Test.@test fmt isa NamedTuple + Test.@test haskey(fmt, :bold) + Test.@test haskey(fmt, :reset) + Test.@test haskey(fmt, :name) + Test.@test haskey(fmt, :type) + Test.@test haskey(fmt, :value) + Test.@test haskey(fmt, :keyword) + Test.@test haskey(fmt, :count) + Test.@test haskey(fmt, :label) + end + end + + return nothing +end + +end # module + +test_core_display() = TestCoreDisplay.test_core_display() \ No newline at end of file