diff --git a/Project.toml b/Project.toml index edf2b8a3..094af33e 100644 --- a/Project.toml +++ b/Project.toml @@ -15,7 +15,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [extensions] CoveragePostprocessing = ["Coverage"] -DocumenterReference = ["Documenter", "MarkdownAST", "Markdown"] +DocumenterReference = ["Documenter", "Markdown", "MarkdownAST"] TestRunner = ["Test"] [extras] diff --git a/docs/api_reference.jl b/docs/api_reference.jl index b902903c..b9c9b3ea 100644 --- a/docs/api_reference.jl +++ b/docs/api_reference.jl @@ -22,6 +22,7 @@ function generate_api_reference(src_dir::String) joinpath("Core", "Core.jl"), joinpath("Core", "default.jl"), joinpath("Core", "types.jl"), joinpath("Core", "matrix_utils.jl"), joinpath("Core", "function_utils.jl"), joinpath("Core", "macros.jl"), + joinpath("Core", "palette.jl"), joinpath("Core", "display.jl"), )), (mod=CTBase.Descriptions, title="Descriptions", filename="descriptions", files=src( joinpath("Descriptions", "Descriptions.jl"), joinpath("Descriptions", "types.jl"), diff --git a/docs/make.jl b/docs/make.jl index cf53a441..6e09b45f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -83,6 +83,7 @@ with_api_reference(src_dir) do api_pages "Test Runner" => joinpath("guide", "test-runner.md"), "Coverage" => joinpath("guide", "coverage.md"), "API Documentation" => joinpath("guide", "api-documentation.md"), + "Color System" => joinpath("guide", "color-system.md"), ], "API Reference" => api_pages, ], diff --git a/docs/src/guide/color-system.md b/docs/src/guide/color-system.md new file mode 100644 index 00000000..35e67e90 --- /dev/null +++ b/docs/src/guide/color-system.md @@ -0,0 +1,234 @@ +# Color System + +```@meta +CurrentModule = CTBase +``` + +CTBase uses ANSI colors to make the output of `show`, `describe`, and error +messages easier to read in a color-capable terminal. This guide explains the +semantic color roles, the built-in themes, and how to customize the color scheme +at runtime. + +```@example color +using CTBase +nothing # hide +``` + +## Overview + +Every color emitted by CTBase comes from a single *active palette* — a +[`Core.Palette`](@ref) struct that maps **semantic roles** to ANSI codes. +All display paths (Strategies, Options, Exceptions, TestRunner) read from the +same palette, so changing it in one place re-skins everything. + +```text +CTBase.Core.set_palette!(theme) + │ + ▼ + _ACTIVE_PALETTE (Ref{Palette}) + │ + ├─► Core.get_format_codes(io) ─► Base.show / describe + └─► Core._red/_yellow/… ─► Exceptions display +``` + +Color is only emitted when the IO object reports `get(io, :color, false) == true` +(the standard Julia terminal check). In a plain `IOBuffer` or a redirected file, +no escape codes are written — regardless of the palette. + +## Semantic Roles + +Each role has a distinct name and meaning. Two roles can share the same default +color, but they are independently configurable: + +| Role | `fmt` key | Default | Meaning | +| --- | --- | --- | --- | +| `name` | `fmt.name` | bold blue | identifiers, type names, option keys | +| `type` | `fmt.type` | cyan | type annotations, hierarchy entries | +| `value` | `fmt.value` | green | data / option values | +| `keyword` | `fmt.keyword` | yellow | Julia symbols (`:gradient`), aliases, IDs | +| `count` | `fmt.count` | magenta | numeric counts, lengths | +| `label` | `fmt.label` | gray | secondary labels, `[default]` / `[user]` tags | +| `emphasis` | `fmt.emphasis` | bold | message text, function names | +| `muted` | `fmt.muted` | dim | structural chars (`│ └─ →`), time suffix | +| `error` | `fmt.error` | red | failures, missing extension messages | +| `warning` | `fmt.warning` | yellow | notable values: `Got`, `Retcode`, skipped test | +| `success` | `fmt.success` | green | positive info: `Hint`, `Expected`, passing test | + +> **Note** `fmt.bold` and `fmt.dim` are legacy aliases for `fmt.emphasis` and +> `fmt.muted` kept for backward compatibility. + +## Built-in Themes + +Three palettes are provided out of the box. Use [`Core.show_palette`](@ref) to +preview any of them in your terminal: + +```julia +using CTBase + +# Preview the default palette +CTBase.Core.show_palette() + +# Compare with high-contrast +CTBase.Core.set_palette!(CTBase.Core.HIGH_CONTRAST_PALETTE) +CTBase.Core.show_palette() +CTBase.Core.reset_palette!() +``` + +`show_palette` prints three sections: the role swatches, a mock `describe`/`show` +block, and a mock exception block — so you see exactly what each role looks like +before committing to a theme. + +### `DEFAULT_PALETTE` + +The standard theme. Colors match Julia's REPL conventions. + +```@example color +CTBase.Core.current_palette() === CTBase.Core.DEFAULT_PALETTE +``` + +```@example color +CTBase.Core.show_palette() +``` + +### `MONOCHROME_PALETTE` + +All roles use an empty code — no color or formatting is ever emitted. Useful +for CI logs, plain-text output, or accessibility contexts. + +```@example color +CTBase.Core.set_palette!(CTBase.Core.MONOCHROME_PALETTE) +CTBase.Core.current_palette() === CTBase.Core.MONOCHROME_PALETTE +``` + +```@example color +CTBase.Core.show_palette() +``` + +```@example color +CTBase.Core.reset_palette!() # back to default +nothing # hide +``` + +### `HIGH_CONTRAST_PALETTE` + +Bright bold variants for better readability on low-contrast terminals or for +users who prefer stronger visual cues. + +```@example color +CTBase.Core.set_palette!(CTBase.Core.HIGH_CONTRAST_PALETTE) +``` + +```@example color +CTBase.Core.show_palette() +``` + +```@example color +CTBase.Core.reset_palette!() +nothing # hide +``` + +## Switching Themes at Runtime + +Use [`Core.set_palette!`](@ref) to swap the active palette, and +[`Core.reset_palette!`](@ref) to restore the default: + +```julia +using CTBase + +# Switch to monochrome (e.g. for automated output) +CTBase.Core.set_palette!(CTBase.Core.MONOCHROME_PALETTE) + +# … all show / describe calls are now plain text … + +# Restore the default +CTBase.Core.reset_palette!() +``` + +The change is **global and immediate** — the next `show` or `describe` call +picks up the new palette. There is no scoped override in the current +implementation. + +## Fine-Grained Overrides + +[`Core.set_color!`](@ref) changes a single role without touching the rest: + +```julia +using CTBase + +# Make errors magenta instead of red +CTBase.Core.set_color!(:error, "35") + +# Suppress the muted dim effect +CTBase.Core.set_color!(:muted, "") + +# Restore everything +CTBase.Core.reset_palette!() +``` + +Valid role symbols are `:name`, `:type`, `:value`, `:keyword`, `:count`, +`:label`, `:emphasis`, `:muted`, `:error`, `:warning`, `:success`. + +The `code` string is the numeric part of the ANSI escape sequence (e.g. `"32"` +for green, `"1;34"` for bold blue, `""` to suppress styling for that role). See +[ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) +for a reference table. + +## Custom Palettes + +Build a [`Core.Palette`](@ref) from scratch using [`Core.Style`](@ref) values: + +```julia +using CTBase + +my_palette = CTBase.Core.Palette( + CTBase.Core.Style("1;35"), # name — bold magenta + CTBase.Core.Style("33"), # type — yellow + CTBase.Core.Style("32"), # value — green + CTBase.Core.Style("36"), # keyword — cyan + CTBase.Core.Style("34"), # count — blue + CTBase.Core.Style("90"), # label — gray + CTBase.Core.Style("1"), # emphasis — bold + CTBase.Core.Style("2"), # muted — dim + CTBase.Core.Style("31"), # error — red + CTBase.Core.Style("33"), # warning — yellow + CTBase.Core.Style("32"), # success — green +) + +CTBase.Core.set_palette!(my_palette) + +# … use CTBase … + +CTBase.Core.reset_palette!() +``` + +Fields must be provided in the order shown in [`Core.Palette`](@ref). + +## Using `get_format_codes` in Custom Display Code + +If you implement `Base.show` for a type that extends CTBase, use +[`Core.get_format_codes`](@ref) to derive styled codes rather than +hardcoding ANSI sequences: + +```julia +function Base.show(io::IO, ::MIME"text/plain", x::MyType) + fmt = CTBase.Core.get_format_codes(io) + print(io, fmt.name, "MyType", fmt.reset, " with value ") + print(io, fmt.value, x.value, fmt.reset) + println(io) +end +``` + +This ensures your display code: + +- respects the active palette (user-selected theme), +- is automatically silenced when the IO does not support color, +- stays consistent with every other CTBase display. + +## See Also + +- [`Core.Style`](@ref), [`Core.Palette`](@ref) — type definitions +- [`Core.DEFAULT_PALETTE`](@ref), [`Core.MONOCHROME_PALETTE`](@ref), [`Core.HIGH_CONTRAST_PALETTE`](@ref) — built-in themes +- [`Core.current_palette`](@ref), [`Core.set_palette!`](@ref), [`Core.reset_palette!`](@ref) — palette switching +- [`Core.set_color!`](@ref) — single-role override +- [`Core.get_format_codes`](@ref) — derive styled codes for custom `show` methods +- [`Core.show_palette`](@ref) — interactive preview of the active palette diff --git a/ext/DocumenterReference/DocumenterReference.jl b/ext/DocumenterReference/DocumenterReference.jl index 7a9529c6..0565db42 100644 --- a/ext/DocumenterReference/DocumenterReference.jl +++ b/ext/DocumenterReference/DocumenterReference.jl @@ -17,6 +17,7 @@ module DocumenterReference using CTBase: CTBase +import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using Documenter: Documenter using Markdown: Markdown using MarkdownAST: MarkdownAST diff --git a/ext/DocumenterReference/config_helpers.jl b/ext/DocumenterReference/config_helpers.jl index f18d8159..0a917f62 100644 --- a/ext/DocumenterReference/config_helpers.jl +++ b/ext/DocumenterReference/config_helpers.jl @@ -1,8 +1,17 @@ """ - _parse_primary_modules(primary_modules::Vector) -> Dict{Module, Vector{String}} + $(TYPEDSIGNATURES) Parse the `primary_modules` argument into a dictionary mapping modules to their source files. Handles both plain modules and `Module => files` pairs. + +# Arguments +- `primary_modules::Vector`: vector of modules or `Module => files` pairs. + +# Returns +- `Dict{Module, Vector{String}}`: dictionary mapping each module to its source file paths. + +# Throws +- `CTBase.Exceptions.IncorrectArgument`: if an element is neither a `Module` nor a `Pair`. """ function _parse_primary_modules(primary_modules::Vector) result = Dict{Module,Vector{String}}() @@ -27,21 +36,46 @@ function _parse_primary_modules(primary_modules::Vector) end """ - _normalize_paths(paths) -> Vector{String} + $(TYPEDSIGNATURES) Normalize a collection of paths to absolute paths. + +# Arguments +- `paths`: collection of file paths to normalize. + +# Returns +- `Vector{String}`: vector of absolute paths, or empty vector if input is empty. """ function _normalize_paths(paths) return isempty(paths) ? String[] : [abspath(p) for p in paths] end """ - _register_config(current_module, subdirectory, modules, sort_by, exclude, public, private, - title, title_in_menu, source_files, filename, include_without_source, - external_modules_to_document, public_title, private_title, - public_description, private_description) + $(TYPEDSIGNATURES) Create and register a `_Config` in the global `CONFIG` vector. + +# Arguments +- `current_module::Module`: the module being documented. +- `subdirectory::String`: output subdirectory for generated documentation. +- `modules::Dict{Module,Vector{String}}`: mapping of modules to their source files. +- `sort_by::Function`: sorting function for symbols. +- `exclude::Set{Symbol}`: symbols to exclude from documentation. +- `public::Bool`: whether to document public API. +- `private::Bool`: whether to document private API. +- `title::String`: page title. +- `title_in_menu::String`: title displayed in navigation menu. +- `source_files::Vector{String}`: global source files for filtering. +- `filename::String`: base filename for generated markdown. +- `include_without_source::Bool`: whether to include symbols without source location. +- `external_modules_to_document::Vector{Module}`: external modules to document. +- `public_title::String`: title for public API section. +- `private_title::String`: title for private API section. +- `public_description::String`: description for public API section. +- `private_description::String`: description for private API section. + +# Returns +- `Nothing`. """ function _register_config( current_module::Module, @@ -88,9 +122,18 @@ function _register_config( end """ - _default_basename(filename::String, public::Bool, private::Bool) -> String + $(TYPEDSIGNATURES) Compute the default base filename for the generated markdown file. + +# Arguments +- `filename::String`: user-provided filename (empty to use default). +- `public::Bool`: whether documenting public API. +- `private::Bool`: whether documenting private API. + +# Returns +- `String`: base filename for the generated markdown file. + """ function _default_basename(filename::String, public::Bool, private::Bool) !isempty(filename) && return filename @@ -100,10 +143,18 @@ function _default_basename(filename::String, public::Bool, private::Bool) end """ - _default_title(public::Bool, private::Bool) -> String + $(TYPEDSIGNATURES) Compute the default title based on public/private flags. Returns empty string for single pages to use configured title. + +# Arguments +- `public::Bool`: whether documenting public API. +- `private::Bool`: whether documenting private API. + +# Returns +- `String`: default title, or empty string to use configured title. + """ function _default_title(public::Bool, private::Bool) public && private && return "API Reference" # Combined page @@ -111,10 +162,18 @@ function _default_title(public::Bool, private::Bool) end """ - _build_page_path(subdirectory::String, filename::String) -> String + $(TYPEDSIGNATURES) Build the page path by joining subdirectory and filename. Handles special cases where `subdirectory` is `"."` or empty. + +# Arguments +- `subdirectory::String`: output subdirectory ("." or empty for current directory). +- `filename::String`: markdown filename. + +# Returns +- `String`: complete page path. + """ function _build_page_path(subdirectory::String, filename::String) (subdirectory == "." || isempty(subdirectory)) && return filename @@ -122,9 +181,20 @@ function _build_page_path(subdirectory::String, filename::String) end """ - _build_page_return_structure(title_in_menu, subdirectory, filename, public, private) -> Pair + $(TYPEDSIGNATURES) Build the return structure for `automatic_reference_documentation`. + +# Arguments +- `title_in_menu::String`: title displayed in navigation menu. +- `subdirectory::String`: output subdirectory. +- `filename::String`: base filename for generated markdown. +- `public::Bool`: whether documenting public API. +- `private::Bool`: whether documenting private API. + +# Returns +- `Pair`: structure suitable for `automatic_reference_documentation`, either a single page or nested public/private pages. + """ function _build_page_return_structure( title_in_menu::String, @@ -146,10 +216,17 @@ function _build_page_return_structure( end """ - _get_effective_source_files(config::_Config) -> Vector{String} + $(TYPEDSIGNATURES) Determine the effective source files for filtering symbols. Priority: module-specific files > global source_files > empty (no filtering). + +# Arguments +- `config::_Config`: configuration object. + +# Returns +- `Vector{String}`: effective source files for the current module. + """ function _get_effective_source_files(config::_Config) module_files = get(config.modules, config.current_module, String[]) diff --git a/ext/DocumenterReference/entry_point.jl b/ext/DocumenterReference/entry_point.jl index 4ebeca60..ad0d00b4 100644 --- a/ext/DocumenterReference/entry_point.jl +++ b/ext/DocumenterReference/entry_point.jl @@ -1,8 +1,12 @@ """ - reset_config!() + $(TYPEDSIGNATURES) Clear the global `CONFIG` vector and `PAGE_CONTENT_ACCUMULATOR`. Useful between documentation builds or for testing. + +# Returns +- `Nothing`. + """ function reset_config!() empty!(CONFIG) @@ -11,56 +15,43 @@ function reset_config!() end """ - automatic_reference_documentation(; - subdirectory::String, - primary_modules, - sort_by::Function = identity, - exclude::Vector{Symbol} = Symbol[], - public::Bool = true, - private::Bool = true, - title::String = "API Reference", - title_in_menu::String = "", - filename::String = "", - source_files::Vector{String} = String[], - include_without_source::Bool = false, - external_modules_to_document::Vector{Module} = Module[], - public_title::String = "", - private_title::String = "", - public_description::String = "", - private_description::String = "", - ) + $(TYPEDSIGNATURES) Automatically creates the API reference documentation for one or more modules and returns a structure which can be used in the `pages` argument of `Documenter.makedocs`. -## Arguments - - * `subdirectory`: the directory relative to the documentation root in which to - write the API files. - * `primary_modules`: a vector of modules or `Module => source_files` pairs to document. - When source files are provided, only symbols defined in those files are documented. - * `sort_by`: a custom sort function applied to symbol lists. - * `exclude`: vector of symbol names to skip from the generated API. - * `public`: flag to generate public API page (default: `true`). - * `private`: flag to generate private API page (default: `true`). - * `title`: title displayed at the top of the generated page. - * `title_in_menu`: title displayed in the navigation menu (default: same as `title`). - * `filename`: base filename (without extension) for the markdown file. - * `source_files`: global source file paths (fallback if no module-specific files). - **Deprecated**: prefer using `primary_modules=[Module => files]` instead. - * `include_without_source`: if `true`, include symbols whose source file cannot - be determined. Default: `false`. - * `external_modules_to_document`: additional modules to search for docstrings - (e.g., `[Plots]` to include `Plots.plot` methods defined in your source files). - * `public_title`: custom title for public API page. Empty string uses default ("Public API" or "Public"). - * `private_title`: custom title for private API page. Empty string uses default ("Private API" or "Private"). - * `public_description`: custom description text for public API page. Empty string uses default. - * `private_description`: custom description text for private API page. Empty string uses default. - -## Multiple instances +# Arguments +- `subdirectory::String`: the directory relative to the documentation root in which to write the API files. +- `primary_modules`: a vector of modules or `Module => source_files` pairs to document. + When source files are provided, only symbols defined in those files are documented. +- `sort_by::Function`: a custom sort function applied to symbol lists. +- `exclude::Vector{Symbol}`: vector of symbol names to skip from the generated API. +- `public::Bool`: flag to generate public API page (default: `true`). +- `private::Bool`: flag to generate private API page (default: `true`). +- `title::String`: title displayed at the top of the generated page. +- `title_in_menu::String`: title displayed in the navigation menu (default: same as `title`). +- `filename::String`: base filename (without extension) for the markdown file. +- `source_files::Vector{String}`: global source file paths (fallback if no module-specific files). + **Deprecated**: prefer using `primary_modules=[Module => files]` instead. +- `include_without_source::Bool`: if `true`, include symbols whose source file cannot be determined. Default: `false`. +- `external_modules_to_document::Vector{Module}`: additional modules to search for docstrings + (e.g., `[Plots]` to include `Plots.plot` methods defined in your source files). +- `public_title::String`: custom title for public API page. Empty string uses default ("Public API" or "Public"). +- `private_title::String`: custom title for private API page. Empty string uses default ("Private API" or "Private"). +- `public_description::String`: custom description text for public API page. Empty string uses default. +- `private_description::String`: custom description text for private API page. Empty string uses default. + +# Returns +- `Pair`: structure suitable for `Documenter.makedocs` pages argument. + +# Throws +- `CTBase.Exceptions.IncorrectArgument`: if both `public` and `private` are `false`. + +# Notes Each time you call this function, a new object is added to the global variable -`DocumenterReference.CONFIG`. Use `reset_config!()` to clear it between builds. +`CONFIG`. Use `reset_config!()` to clear it between builds. + """ function CTBase.automatic_reference_documentation( ::CTBase.DevTools.DocumenterReferenceTag; diff --git a/ext/DocumenterReference/page_building.jl b/ext/DocumenterReference/page_building.jl index ba9e6c23..bbef305b 100644 --- a/ext/DocumenterReference/page_building.jl +++ b/ext/DocumenterReference/page_building.jl @@ -1,8 +1,16 @@ """ - _build_api_page(document::Documenter.Document, config::_Config) + $(TYPEDSIGNATURES) Generate public and/or private API reference pages for a module. Accumulates content in `PAGE_CONTENT_ACCUMULATOR` for later finalization. + +# Arguments +- `document::Documenter.Document`: the Documenter document object. +- `config::_Config`: configuration for the API page generation. + +# Returns +- `Nothing`. + """ function _build_api_page(document::Documenter.Document, config::_Config) current_module = config.current_module @@ -64,9 +72,18 @@ function _build_api_page(document::Documenter.Document, config::_Config) end """ - _collect_module_docstrings(config::_Config, symbol_list) -> Vector{String} + $(TYPEDSIGNATURES) Collect docstring blocks for symbols from the current module. + +# Arguments +- `config::_Config`: configuration for documentation generation. +- `symbol_list`: list of symbols to document. +- `include_module::Bool`: whether to include the module docstring (default: `true`). + +# Returns +- `Vector{String}`: docstring blocks formatted for Documenter. + """ function _collect_module_docstrings(config::_Config, symbol_list; include_module::Bool=true) docstrings = String[] @@ -85,12 +102,13 @@ function _collect_module_docstrings(config::_Config, symbol_list; include_module effective_source_files, config.include_without_source, ) - push!(docstrings, "### `$(current_module)`\n\n```@docs\n$(current_module)\n```\n\n") + push!(docstrings, "### `$(current_module)` [Module]\n\n```@docs\n$(current_module)\n```\n\n") end _iterate_over_symbols(config, symbol_list) do key, type type == DOCTYPE_MODULE && return nothing - push!(docstrings, "### `$key`\n\n```@docs\n$(current_module).$key\n```\n\n") + label = titlecase(DOCTYPE_NAMES[type]) + push!(docstrings, "### `$key` [$label]\n\n```@docs\n$(current_module).$key\n```\n\n") return nothing end @@ -98,9 +116,17 @@ function _collect_module_docstrings(config::_Config, symbol_list; include_module end """ - _collect_private_docstrings(config::_Config, symbol_list) -> Vector{String} + $(TYPEDSIGNATURES) Collect docstring blocks for private symbols, including external module methods. + +# Arguments +- `config::_Config`: configuration for documentation generation. +- `symbol_list`: list of private symbols to document. + +# Returns +- `Vector{String}`: docstring blocks formatted for Documenter. + """ function _collect_private_docstrings(config::_Config, symbol_list) docstrings = _collect_module_docstrings(config, symbol_list; include_module=false) @@ -115,9 +141,16 @@ function _collect_private_docstrings(config::_Config, symbol_list) end """ - _collect_external_module_docstrings(config::_Config) -> Vector{String} + $(TYPEDSIGNATURES) Collect docstrings for methods from external modules defined in source files. + +# Arguments +- `config::_Config`: configuration for documentation generation. + +# Returns +- `Vector{String}`: docstring blocks for external module methods. + """ function _collect_external_module_docstrings(config::_Config) docstrings = String[] @@ -144,9 +177,17 @@ function _collect_external_module_docstrings(config::_Config) end """ - _collect_methods_from_source_files(mod::Module, source_files::Vector{String}) -> Dict{Symbol, Vector{Method}} + $(TYPEDSIGNATURES) Collect all methods from a module that are defined in the given source files. + +# Arguments +- `mod::Module`: the module to search for methods. +- `source_files::Vector{String}`: source file paths to filter by. + +# Returns +- `Dict{Symbol, Vector{Method}}`: mapping of function names to their methods. + """ function _collect_methods_from_source_files(mod::Module, source_files::Vector{String}) methods_by_func = Dict{Symbol,Vector{Method}}() @@ -180,9 +221,16 @@ function _collect_methods_from_source_files(mod::Module, source_files::Vector{St end """ - _finalize_api_pages(document::Documenter.Document) + $(TYPEDSIGNATURES) Finalize all accumulated API pages by combining content from multiple modules. + +# Arguments +- `document::Documenter.Document`: the Documenter document object. + +# Returns +- `Nothing`. + """ function _finalize_api_pages(document::Documenter.Document) for (filename, module_contents) in PAGE_CONTENT_ACCUMULATOR @@ -284,9 +332,17 @@ function _finalize_api_pages(document::Documenter.Document) end """ - _build_combined_page_content(modules_str, module_contents) -> Tuple{String, Vector{String}} + $(TYPEDSIGNATURES) Build the overview and docstrings for a combined (Public + Private) API page. + +# Arguments +- `modules_str::String`: comma-separated list of module names. +- `module_contents`: vector of (module, public_docs, private_docs) tuples. + +# Returns +- `Tuple{String, Vector{String}}`: overview markdown and docstring blocks. + """ function _build_combined_page_content(modules_str::String, module_contents) overview = """ @@ -315,16 +371,20 @@ function _build_combined_page_content(modules_str::String, module_contents) end """ - _build_private_page_content(modules_str, module_contents, is_split; custom_title="", custom_description="") -> Tuple{String, Vector{String}} + $(TYPEDSIGNATURES) Build the overview and docstrings for a private API page. # Arguments -- `modules_str`: Comma-separated list of module names -- `module_contents`: Vector of (module, public_docs, private_docs) tuples -- `is_split`: Whether this is part of a split public/private documentation -- `custom_title`: Optional custom title (empty string uses default) -- `custom_description`: Optional custom description (empty string uses default) +- `modules_str::String`: comma-separated list of module names. +- `module_contents`: vector of (module, public_docs, private_docs) tuples. +- `is_split::Bool`: whether this is part of a split public/private documentation. +- `custom_title::String`: optional custom title (empty string uses default). +- `custom_description::String`: optional custom description (empty string uses default). + +# Returns +- `Tuple{String, Vector{String}}`: overview markdown and docstring blocks. + """ function _build_private_page_content( modules_str::String, @@ -370,16 +430,20 @@ function _build_private_page_content( end """ - _build_public_page_content(modules_str, module_contents, is_split; custom_title="", custom_description="") -> Tuple{String, Vector{String}} + $(TYPEDSIGNATURES) Build the overview and docstrings for a public API page. # Arguments -- `modules_str`: Comma-separated list of module names -- `module_contents`: Vector of (module, public_docs, private_docs) tuples -- `is_split`: Whether this is part of a split public/private documentation -- `custom_title`: Optional custom title (empty string uses default) -- `custom_description`: Optional custom description (empty string uses default) +- `modules_str::String`: comma-separated list of module names. +- `module_contents`: vector of (module, public_docs, private_docs) tuples. +- `is_split::Bool`: whether this is part of a split public/private documentation. +- `custom_title::String`: optional custom title (empty string uses default). +- `custom_description::String`: optional custom description (empty string uses default). + +# Returns +- `Tuple{String, Vector{String}}`: overview markdown and docstring blocks. + """ function _build_public_page_content( modules_str::String, diff --git a/ext/DocumenterReference/source_file_detection.jl b/ext/DocumenterReference/source_file_detection.jl index 2383f936..27e69bb3 100644 --- a/ext/DocumenterReference/source_file_detection.jl +++ b/ext/DocumenterReference/source_file_detection.jl @@ -1,8 +1,17 @@ """ - _get_source_file(mod::Module, key::Symbol, type::DocType) -> Union{String, Nothing} + $(TYPEDSIGNATURES) Determine the source file path where a symbol is defined. Returns `nothing` if the source file cannot be determined. + +# Arguments +- `mod::Module`: the module containing the symbol. +- `key::Symbol`: the symbol name. +- `type::DocType`: the type of the symbol. + +# Returns +- `Union{String, Nothing}`: absolute path to the source file, or `nothing` if undetermined. + """ function _get_source_file(mod::Module, key::Symbol, type::DocType) try @@ -32,9 +41,17 @@ function _get_source_file(mod::Module, key::Symbol, type::DocType) end """ - _get_source_from_docstring(mod::Module, key::Symbol) -> Union{String, Nothing} + $(TYPEDSIGNATURES) Try to get source file path from docstring metadata. + +# Arguments +- `mod::Module`: the module containing the symbol. +- `key::Symbol`: the symbol name. + +# Returns +- `Union{String, Nothing}`: absolute path to the source file, or `nothing` if not found. + """ function _get_source_from_docstring(mod::Module, key::Symbol) binding = Base.Docs.Binding(mod, key) @@ -56,9 +73,16 @@ function _get_source_from_docstring(mod::Module, key::Symbol) end """ - _get_source_from_methods(obj) -> Union{String, Nothing} + $(TYPEDSIGNATURES) Try to get source file path from method definitions. + +# Arguments +- `obj`: the function or type object to inspect. + +# Returns +- `Union{String, Nothing}`: absolute path to the source file, or `nothing` if not found. + """ function _get_source_from_methods(obj) for m in methods(obj) diff --git a/ext/DocumenterReference/symbol_classification.jl b/ext/DocumenterReference/symbol_classification.jl index f76fc19c..da5f31b2 100644 --- a/ext/DocumenterReference/symbol_classification.jl +++ b/ext/DocumenterReference/symbol_classification.jl @@ -1,14 +1,29 @@ """ - _to_string(x::DocType) -> String + $(TYPEDSIGNATURES) Convert a DocType enumeration value to its string representation. + +# Arguments +- `x::DocType`: the DocType value to convert. + +# Returns +- `String`: human-readable string representation. + """ _to_string(x::DocType) = DOCTYPE_NAMES[x] """ - _classify_symbol(obj, name_str::String) -> DocType + $(TYPEDSIGNATURES) Classify a symbol by its type (function, macro, struct, constant, module, abstract type). + +# Arguments +- `obj`: the symbol object to classify. +- `name_str::String`: the name of the symbol as a string. + +# Returns +- `DocType`: the classification of the symbol. + """ function _classify_symbol(obj, name_str::String) startswith(name_str, "@") && return DOCTYPE_MACRO @@ -20,11 +35,18 @@ function _classify_symbol(obj, name_str::String) end """ - _exported_symbols(mod::Module) -> NamedTuple + $(TYPEDSIGNATURES) Classify all symbols in a module into exported and private categories. Returns a NamedTuple with `exported` and `private` fields, each containing sorted lists of `(Symbol, DocType)` pairs. + +# Arguments +- `mod::Module`: the module to analyze. + +# Returns +- `NamedTuple`: with `exported` and `private` fields, each containing sorted lists of `(Symbol, DocType)` pairs. + """ function _exported_symbols(mod::Module) exported = Pair{Symbol,DocType}[] diff --git a/ext/DocumenterReference/symbol_iteration.jl b/ext/DocumenterReference/symbol_iteration.jl index 259024fa..5721a7e7 100644 --- a/ext/DocumenterReference/symbol_iteration.jl +++ b/ext/DocumenterReference/symbol_iteration.jl @@ -1,8 +1,17 @@ """ - _iterate_over_symbols(f, config, symbol_list) + $(TYPEDSIGNATURES) Iterate over symbols, applying a function to each documented symbol. Filters symbols based on exclusion list, documentation presence, and source files. + +# Arguments +- `f`: function to apply to each symbol, receives `(key::Symbol, type::DocType)`. +- `config::_Config`: configuration for documentation generation. +- `symbol_list`: list of symbols to iterate over. + +# Returns +- `Nothing`. + """ function _iterate_over_symbols(f, config::_Config, symbol_list) current_module = config.current_module @@ -32,9 +41,19 @@ function _iterate_over_symbols(f, config::_Config, symbol_list) end """ - _has_documentation(mod::Module, key::Symbol, type::DocType, modules::Dict) -> Bool + $(TYPEDSIGNATURES) Check if a symbol has documentation. Logs a warning if not. + +# Arguments +- `mod::Module`: the module containing the symbol. +- `key::Symbol`: the symbol name. +- `type::DocType`: the type of the symbol. +- `modules::Dict`: mapping of modules to their source files. + +# Returns +- `Bool`: `true` if the symbol has documentation, `false` otherwise. + """ function _has_documentation(mod::Module, key::Symbol, type::DocType, modules::Dict) binding = Base.Docs.Binding(mod, key) @@ -60,9 +79,20 @@ function _has_documentation(mod::Module, key::Symbol, type::DocType, modules::Di end """ - _passes_source_filter(mod, key, type, source_files, include_without_source) -> Bool + $(TYPEDSIGNATURES) Check if a symbol passes the source file filter. + +# Arguments +- `mod::Module`: the module containing the symbol. +- `key::Symbol`: the symbol name. +- `type::DocType`: the type of the symbol. +- `source_files::Vector{String}`: source file paths to filter by. +- `include_without_source::Bool`: whether to include symbols without source location. + +# Returns +- `Bool`: `true` if the symbol passes the filter, `false` otherwise. + """ function _passes_source_filter( mod::Module, diff --git a/ext/DocumenterReference/type_formatting.jl b/ext/DocumenterReference/type_formatting.jl index be7b80d0..683ba249 100644 --- a/ext/DocumenterReference/type_formatting.jl +++ b/ext/DocumenterReference/type_formatting.jl @@ -1,8 +1,17 @@ """ - _method_signature_string(m::Method, mod::Module, key::Symbol) -> String + $(TYPEDSIGNATURES) Generate a Documenter-compatible signature string for a method. Returns a string like `Module.func(::Type1, ::Type2)` for use in `@docs` blocks. + +# Arguments +- `m::Method`: the method to format. +- `mod::Module`: the module containing the method. +- `key::Symbol`: the function name. + +# Returns +- `String`: Documenter-compatible signature string. + """ function _method_signature_string(m::Method, mod::Module, key::Symbol) sig = m.sig @@ -26,10 +35,17 @@ function _method_signature_string(m::Method, mod::Module, key::Symbol) end """ - _format_type_for_docs(T) -> String + $(TYPEDSIGNATURES) Format a type for use in Documenter's `@docs` block. Always fully qualifies types to avoid UndefVarError when Documenter evaluates in Main. + +# Arguments +- `T`: the type to format. + +# Returns +- `String`: formatted type string with `::` prefix and full module qualification. + """ function _format_type_for_docs(T) # Vararg @@ -62,9 +78,16 @@ function _format_type_for_docs(T) end """ - _format_datatype_for_docs(T::DataType) -> String + $(TYPEDSIGNATURES) Format a DataType for use in @docs blocks. + +# Arguments +- `T::DataType`: the DataType to format. + +# Returns +- `String`: formatted DataType string with full module qualification. + """ function _format_datatype_for_docs(T::DataType) type_mod = parentmodule(T) @@ -101,9 +124,16 @@ function _format_datatype_for_docs(T::DataType) end """ - _format_type_param(p) -> String + $(TYPEDSIGNATURES) Format a type parameter (can be a type or a value like an integer). + +# Arguments +- `p`: the type parameter to format. + +# Returns +- `String`: formatted type parameter string. + """ function _format_type_param(p) if p isa Type diff --git a/ext/DocumenterReference/types.jl b/ext/DocumenterReference/types.jl index d467354b..d8684713 100644 --- a/ext/DocumenterReference/types.jl +++ b/ext/DocumenterReference/types.jl @@ -1,5 +1,5 @@ """ - DocType +$(TYPEDEF) Enumeration of documentation element types recognized by the API reference generator. @@ -11,6 +11,7 @@ Enumeration of documentation element types recognized by the API reference gener - `DOCTYPE_MACRO`: A macro (name starts with `@`) - `DOCTYPE_MODULE`: A submodule - `DOCTYPE_STRUCT`: A concrete struct type + """ @enum( DocType, @@ -26,6 +27,7 @@ Enumeration of documentation element types recognized by the API reference gener DOCTYPE_NAMES::Dict{DocType, String} Mapping from DocType enum values to their human-readable string representations. + """ const DOCTYPE_NAMES = Dict{DocType,String}( DOCTYPE_ABSTRACT_TYPE => "abstract type", @@ -41,6 +43,7 @@ const DOCTYPE_NAMES = Dict{DocType,String}( Ordering for DocType values used when sorting symbols for display. Lower values appear first. + """ const DOCTYPE_ORDER = Dict{DocType,Int}( DOCTYPE_MODULE => 0, @@ -52,7 +55,7 @@ const DOCTYPE_ORDER = Dict{DocType,Int}( ) """ - _Config +$(TYPEDEF) Internal configuration for API reference generation. @@ -76,6 +79,7 @@ Internal configuration for API reference generation. - `private_title::String`: Custom title for private API page (empty string uses default). - `public_description::String`: Custom description for public API page (empty string uses default). - `private_description::String`: Custom description for private API page (empty string uses default). + """ struct _Config current_module::Module @@ -104,6 +108,7 @@ Global configuration storage for API reference generation. Each call to `CTBase.automatic_reference_documentation` appends a new `_Config` entry to this vector. Use `reset_config!` to clear it between builds. + """ const CONFIG = _Config[] @@ -112,6 +117,7 @@ const CONFIG = _Config[] Global accumulator for multi-module combined pages. Maps output filename to a list of (module, public_docstrings, private_docstrings) tuples. + """ const PAGE_CONTENT_ACCUMULATOR = Dict{ String,Vector{Tuple{Module,Vector{String},Vector{String}}} diff --git a/ext/TestRunner/progress.jl b/ext/TestRunner/progress.jl index b17a5df3..fc74c460 100644 --- a/ext/TestRunner/progress.jl +++ b/ext/TestRunner/progress.jl @@ -102,20 +102,24 @@ Internal helper to map test status to severity level for display formatting. (status == :error || status == :test_failed) ? 3 : (status == :skipped ? 2 : 1) """ - _color_for_severity(sev::Int) -> String + _color_for_severity(sev::Int, io::IO) -> String -Internal helper to map severity level to ANSI color code. +Internal helper to map severity level to an ANSI opening code, respecting the +colour capability of `io` and the active CTBase colour palette. # Arguments - `sev::Int`: Severity level (3=failure, 2=skipped, 1=success) +- `io::IO`: Output stream (checked for colour support) # Returns -- `String`: ANSI color escape code (red for failure, yellow for skipped, green for success) +- `String`: ANSI colour escape code, or empty string when colour is disabled """ -@inline function _color_for_severity(sev::Int) - sev >= 3 && return "\e[31m" # red - sev == 2 && return "\e[33m" # yellow - return "\e[32m" # green +@inline function _color_for_severity(sev::Int, io::IO) + get(io, :color, false) || return "" + p = CTBase.Core.current_palette() + st = sev >= 3 ? p.error : (sev == 2 ? p.warning : p.success) + isempty(st.code) && return "" + return "\033[$(st.code)m" end """ @@ -145,7 +149,10 @@ $(TYPEDSIGNATURES) Write a styled progress line for a completed test to `io`. -Uses ANSI colors: green for success, red for errors, yellow for skipped. +Uses the active CTBase colour palette (see `CTBase.Core.set_palette!`): the +`success`, `warning`, and `error` roles drive the status colour; `type` drives +the index; `emphasis` and `muted` drive boldness and the time suffix. +Colour is only emitted when `get(io, :color, false)` is true. # Arguments - `io::IO`: Output stream to write to @@ -157,7 +164,6 @@ Uses ANSI colors: green for success, red for errors, yellow for skipped. # Notes - Format: `[progress_bar] symbol [index/total] spec (time) status` -- Colors: green for success, red for errors, yellow for skipped - Time is displayed with one decimal place when available - **Cursor-style display**: In full-resolution mode (total ≤ threshold), only the current test position is filled for successes, while failures and skips persist at their positions. This creates a lighter, cursor-like visual where past successes are ephemeral. @@ -166,12 +172,12 @@ Uses ANSI colors: green for success, red for errors, yellow for skipped. julia> using CTBase.TestRunner, IOBuffer julia> info = TestRunner.TestRunInfo( - :test_example, - "/path/to/test.jl", - :test_example, - 5, 10, - :post_eval, - nothing, + :test_example, + "/path/to/test.jl", + :test_example, + 5, 10, + :post_eval, + nothing, 1.23 ); @@ -189,27 +195,22 @@ function _format_progress_line( progress_bar_threshold::Int=_PROGRESS_BAR_THRESHOLD, show_progress_bar::Bool=true, ) - # ANSI codes - reset = "\e[0m" - bold = "\e[1m" - dim = "\e[2m" - green = "\e[32m" - red = "\e[31m" - yellow = "\e[33m" - cyan = "\e[36m" + # Derive styled codes from active palette, respecting IO colour capability + fmt = CTBase.Core.get_format_codes(io) + reset = fmt.reset + bold = fmt.emphasis + dim = fmt.muted bar_width = _bar_width(info.total, progress_bar_threshold) bar = _progress_bar(info.index, info.total; width=bar_width) severity = _severity(info.status) + color = _color_for_severity(severity, io) if severity == 3 - color = red symbol = "✗" elseif severity == 2 - color = yellow symbol = "○" else - color = green symbol = "✓" end @@ -221,16 +222,17 @@ function _format_progress_line( "" end status_str = if (info.status == :error || info.status == :test_failed) - " $(bold)$(red)FAILED$(reset)$(dim)," + err_color = _color_for_severity(3, io) + " $(bold)$(err_color)FAILED$(reset)$(dim)," else "" end function bracket_color_from(sev::Union{Int,Nothing}) - sev === nothing && return green - sev <= 1 && return green - sev == 2 && return yellow - return red + sev === nothing && return _color_for_severity(1, io) + sev <= 1 && return _color_for_severity(1, io) + sev == 2 && return _color_for_severity(2, io) + return _color_for_severity(3, io) end has_history = @@ -247,11 +249,11 @@ function _format_progress_line( elseif sev >= 2 # Failures and skips persist at their positions glyph = _block_char_for_severity(sev) - push!(blocks, "$( _color_for_severity(sev) )$(glyph)$(reset)") + push!(blocks, "$(_color_for_severity(sev, io))$(glyph)$(reset)") elseif i == info.index # Current test (success) is shown glyph = _block_char_for_severity(sev) - push!(blocks, "$( _color_for_severity(sev) )$(glyph)$(reset)") + push!(blocks, "$(_color_for_severity(sev, io))$(glyph)$(reset)") else # Past successes are cleared (ephemeral cursor style) push!(blocks, "$(dim)░$(reset)") @@ -277,7 +279,7 @@ function _format_progress_line( ) end print(io, "$(bold)$(color)$(symbol)$(reset) ") - print(io, "$(cyan)$(idx_str)$(reset) ") + print(io, "$(fmt.type)$(idx_str)$(reset) ") print(io, "$(bold)$(info.spec)$(reset)") println(io, "$(status_str)$(time_str)") return nothing @@ -310,12 +312,12 @@ julia> using CTBase.TestRunner julia> cb = TestRunner._make_default_on_test_done(stdout, 10) julia> info = TestRunner.TestRunInfo( - :test_example, - "/path/to/test.jl", - :test_example, - 5, 10, - :post_eval, - nothing, + :test_example, + "/path/to/test.jl", + :test_example, + 5, 10, + :post_eval, + nothing, 1.23 ); diff --git a/src/Core/Core.jl b/src/Core/Core.jl index 93bc5013..55198526 100644 --- a/src/Core/Core.jl +++ b/src/Core/Core.jl @@ -20,9 +20,14 @@ include("macros.jl") # Public utilities include("matrix_utils.jl") +include("palette.jl") include("display.jl") # Export public API export ctNumber, matrix2vec, to_out_of_place, @ensure +export Style, Palette +export DEFAULT_PALETTE, MONOCHROME_PALETTE, HIGH_CONTRAST_PALETTE +export current_palette, set_palette!, set_color!, reset_palette!, show_palette +export get_format_codes end # module diff --git a/src/Core/display.jl b/src/Core/display.jl index ee17b832..eaf37573 100644 --- a/src/Core/display.jl +++ b/src/Core/display.jl @@ -1,131 +1,149 @@ """ - _apply_ansi(s, code, io::IO) +$(TYPEDSIGNATURES) -Apply ANSI escape codes to a string if color is enabled in the IO context. +Apply a [`CTBase.Core.Style`](@ref) to string `s`, respecting the IO colour capability. -# 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 `s` wrapped in ANSI escape codes when `get(io, :color, false)` is `true` +and `st.code` is non-empty; returns the plain string otherwise. -# Returns -- `String`: The string wrapped in ANSI escape codes if `get(io, :color, false)` is true, - otherwise the plain string. +See also: [`CTBase.Core._apply_ansi`](@ref), [`CTBase.Core.get_format_codes`](@ref) """ -_apply_ansi(s, code, io::IO) = get(io, :color, false) ? "\033[$(code)m$(s)\033[0m" : s +function _style(st::Style, s, io::IO) + get(io, :color, false) && !isempty(st.code) || return string(s) + return "\033[$(st.code)m$(s)\033[0m" +end """ $(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. +Apply a raw ANSI numeric code string to `s`, respecting the IO colour capability. -# Returns -- `String`: The string wrapped in dim ANSI escape codes if color is enabled. +Prefer [`CTBase.Core._style`](@ref) with a [`CTBase.Core.Style`](@ref) from the +active palette over calling this function directly. """ -_dim(s, io::IO) = _apply_ansi(s, "2", io) +_apply_ansi(s, code, io::IO) = get(io, :color, false) ? "\033[$(code)m$(s)\033[0m" : string(s) + +# --------------------------------------------------------------------------- +# Named helpers — backed by the active palette roles +# --------------------------------------------------------------------------- """ $(TYPEDSIGNATURES) -Apply bold ANSI styling to a string. - -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. +Apply the `muted` role style from the active palette. -# Returns -- `String`: The string wrapped in bold ANSI escape codes if color is enabled. +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core._style`](@ref) """ -_bold(s, io::IO) = _apply_ansi(s, "1", io) +_dim(s, io::IO) = _style(current_palette().muted, s, 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. +Apply the `emphasis` role style from the active palette. -# Returns -- `String`: The string wrapped in red ANSI escape codes if color is enabled. +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core._style`](@ref) """ -_red(s, io::IO) = _apply_ansi(s, "1;31", io) +_bold(s, io::IO) = _style(current_palette().emphasis, s, 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. +Apply the `error` role style from the active palette (defaults to red). -# Returns -- `String`: The string wrapped in yellow ANSI escape codes if color is enabled. +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core._style`](@ref) """ -_yellow(s, io::IO) = _apply_ansi(s, "33", io) +_red(s, io::IO) = _style(current_palette().error, s, io) """ $(TYPEDSIGNATURES) -Apply green ANSI styling to a string. +Apply the `warning` role style from the active palette (defaults to yellow). -# Arguments -- `s::AbstractString`: The string to style. -- `io::IO`: Output stream to check for color support. +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core._style`](@ref) +""" +_yellow(s, io::IO) = _style(current_palette().warning, s, io) -# Returns -- `String`: The string wrapped in green ANSI escape codes if color is enabled. """ -_green(s, io::IO) = _apply_ansi(s, "32", io) +$(TYPEDSIGNATURES) + +Apply the `success` role style from the active palette (defaults to green). +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core._style`](@ref) """ - get_format_codes(io::IO) -> NamedTuple +_green(s, io::IO) = _style(current_palette().success, s, io) -Get ANSI formatting codes based on terminal color support. +# --------------------------------------------------------------------------- +# Structured format codes NamedTuple +# --------------------------------------------------------------------------- -Returns a NamedTuple with formatting codes for consistent display across all show() methods. +""" +$(TYPEDSIGNATURES) -# 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 +Return ANSI opening codes for every semantic role in the active +[`CTBase.Core.Palette`](@ref), respecting the colour capability of `io`. + +Each field is an *opening* escape sequence; callers must close styling with +the `reset` field. Returns empty strings for all codes when `get(io, :color, +false)` is `false`, or when the active palette has an empty code for that role +(e.g. [`CTBase.Core.MONOCHROME_PALETTE`](@ref)). + +# Returns + +A `NamedTuple` with the following fields: + +| Field | Default colour | Semantic role | +| --- | --- | --- | +| `name` | bold blue | identifiers, type names, option keys | +| `type` | cyan | type annotations, hierarchy entries | +| `value` | green | data values | +| `keyword` | yellow | Julia symbols, aliases | +| `count` | magenta | numeric counts | +| `label` | gray | secondary labels, metadata tags | +| `emphasis` / `bold` | bold | message text, function names | +| `muted` / `dim` | dim | structural chars, time suffix | +| `error` | red | failures, missing extensions | +| `warning` | yellow | notable attention values | +| `success` | green | positive hints, expected values | +| `reset` | — | resets all styling | + +`bold` and `dim` are legacy aliases for `emphasis` and `muted`; prefer the +semantic names in new code. # Example -```julia -fmt = get_format_codes(io) -print(io, fmt.name, "option_name", fmt.reset, "::", fmt.type, "Int", fmt.reset) + +```julia-repl +julia> using CTBase + +julia> io = IOContext(stdout, :color => true); + +julia> fmt = CTBase.Core.get_format_codes(io); + +julia> print(io, fmt.name, "option_name", fmt.reset, "::", fmt.type, "Int", fmt.reset) +option_name::Int ``` -# 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 +See also: [`CTBase.Core.set_palette!`](@ref), [`CTBase.Core.Palette`](@ref) """ function get_format_codes(io::IO) - supports_color = get(io, :color, false) - + p = current_palette() + open(st) = get(io, :color, false) && !isempty(st.code) ? "\033[$(st.code)m" : "" + rst = get(io, :color, false) ? "\033[0m" : "" 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 + # Semantic roles + name = open(p.name), + type = open(p.type), + value = open(p.value), + keyword = open(p.keyword), + count = open(p.count), + label = open(p.label), + emphasis = open(p.emphasis), + muted = open(p.muted), + error = open(p.error), + warning = open(p.warning), + success = open(p.success), + reset = rst, + # Legacy aliases + bold = open(p.emphasis), + dim = open(p.muted), ) end diff --git a/src/Core/palette.jl b/src/Core/palette.jl new file mode 100644 index 00000000..02c99fb2 --- /dev/null +++ b/src/Core/palette.jl @@ -0,0 +1,382 @@ +""" +$(TYPEDEF) + +An ANSI display style described by a numeric escape code. + +The `code` field holds the numeric part of the escape sequence (e.g. `"32"` for +green, `"1;34"` for bold blue). An empty string means *no styling* — used by +monochrome palettes and when colour is disabled. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.Style("32") # green +Style("32") + +julia> CTBase.Core.Style("") # no styling +Style("") +``` + +See also: [`CTBase.Core.Palette`](@ref), [`CTBase.Core.set_color!`](@ref) +""" +struct Style + code::String +end + +""" +$(TYPEDEF) + +A complete set of display styles, one per semantic role. + +Each field is a [`CTBase.Core.Style`](@ref) that governs how a particular category of +information is rendered. The active palette is read at every call to +[`CTBase.Core.get_format_codes`](@ref), so swapping it with +[`CTBase.Core.set_palette!`](@ref) takes effect immediately for subsequent `show` calls. + +# Fields + +- `name`: identifiers, type names, option keys (default: bold blue) +- `type`: type annotations, hierarchy entries (default: cyan) +- `value`: data values, option values (default: green) +- `keyword`: Julia symbols (`:euler`), aliases, IDs (default: yellow) +- `count`: numeric counts (default: magenta) +- `label`: secondary labels, metadata tags (default: gray) +- `emphasis`: message text, function names (default: bold) +- `muted`: structural chars (`│`, `└─`, `→`), time suffix (default: dim) +- `error`: failures, missing extensions (default: red) +- `warning`: notable values (`Got`, `Retcode`, skipped test) (default: yellow) +- `success`: positive hints, `Expected`, passing test (default: green) + +See also: [`CTBase.Core.DEFAULT_PALETTE`](@ref), [`CTBase.Core.MONOCHROME_PALETTE`](@ref), [`CTBase.Core.HIGH_CONTRAST_PALETTE`](@ref), [`CTBase.Core.set_palette!`](@ref) +""" +struct Palette + name::Style + type::Style + value::Style + keyword::Style + count::Style + label::Style + emphasis::Style + muted::Style + error::Style + warning::Style + success::Style +end + +""" +The standard colour palette used out of the box. + +Maps each semantic role to a colour that mirrors Julia's REPL conventions +(green for values, cyan for types, etc.). + +See also: [`CTBase.Core.Palette`](@ref), [`CTBase.Core.MONOCHROME_PALETTE`](@ref), [`CTBase.Core.HIGH_CONTRAST_PALETTE`](@ref), [`CTBase.Core.set_palette!`](@ref) +""" +const DEFAULT_PALETTE = Palette( + Style("1;34"), # name — bold blue + Style("36"), # type — cyan + Style("32"), # value — green + Style("33"), # keyword — yellow + Style("35"), # count — magenta + Style("90"), # label — bright black / gray + Style("1"), # emphasis — bold + Style("2"), # muted — dim + Style("31"), # error — red + Style("33"), # warning — yellow + Style("32"), # success — green +) + +""" +A palette with every style set to the empty code. + +No colour or formatting is ever emitted, regardless of terminal capability. +Useful for CI logs, plain-text output, or accessibility contexts where styled +text is unwanted. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.set_palette!(CTBase.Core.MONOCHROME_PALETTE) +``` + +See also: [`CTBase.Core.DEFAULT_PALETTE`](@ref), [`CTBase.Core.HIGH_CONTRAST_PALETTE`](@ref), [`CTBase.Core.reset_palette!`](@ref) +""" +const MONOCHROME_PALETTE = Palette( + Style(""), Style(""), Style(""), Style(""), Style(""), Style(""), + Style(""), Style(""), Style(""), Style(""), Style(""), +) + +""" +A palette using bright, bold variants for improved readability. + +Useful on terminals with poor contrast or for users who prefer stronger colour +cues. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.set_palette!(CTBase.Core.HIGH_CONTRAST_PALETTE) +``` + +See also: [`CTBase.Core.DEFAULT_PALETTE`](@ref), [`CTBase.Core.MONOCHROME_PALETTE`](@ref), [`CTBase.Core.reset_palette!`](@ref) +""" +const HIGH_CONTRAST_PALETTE = Palette( + Style("1;94"), # name — bright bold blue + Style("1;96"), # type — bright bold cyan + Style("1;92"), # value — bright bold green + Style("1;93"), # keyword — bright bold yellow + Style("1;95"), # count — bright bold magenta + Style("37"), # label — white + Style("1"), # emphasis — bold + Style("2"), # muted — dim + Style("1;91"), # error — bright bold red + Style("1;93"), # warning — bright bold yellow + Style("1;92"), # success — bright bold green +) + +# --------------------------------------------------------------------------- +# Runtime global +# --------------------------------------------------------------------------- + +""" +Runtime reference to the currently active palette. + +Initialised to [`CTBase.Core.DEFAULT_PALETTE`](@ref). All display paths read +from this reference; mutate it via [`CTBase.Core.set_palette!`](@ref) and +[`CTBase.Core.reset_palette!`](@ref). +""" +const _ACTIVE_PALETTE = Ref{Palette}(DEFAULT_PALETTE) + +""" +$(TYPEDSIGNATURES) + +Return the currently active [`CTBase.Core.Palette`](@ref). + +The active palette is used by every `show` and `describe` call in CTBase to +derive ANSI codes. Change it with [`CTBase.Core.set_palette!`](@ref). + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.current_palette() === CTBase.Core.DEFAULT_PALETTE +true +``` + +See also: [`CTBase.Core.set_palette!`](@ref), [`CTBase.Core.reset_palette!`](@ref) +""" +current_palette() = _ACTIVE_PALETTE[] + +""" +$(TYPEDSIGNATURES) + +Replace the active [`CTBase.Core.Palette`](@ref) with `p` and return `p`. + +The change is global and immediate: the next `show` or `describe` call uses +the new palette. Use [`CTBase.Core.reset_palette!`](@ref) to restore the default. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.set_palette!(CTBase.Core.HIGH_CONTRAST_PALETTE) + +julia> CTBase.Core.current_palette() === CTBase.Core.HIGH_CONTRAST_PALETTE +true + +julia> CTBase.Core.reset_palette!() +``` + +See also: [`CTBase.Core.current_palette`](@ref), [`CTBase.Core.reset_palette!`](@ref) +""" +function set_palette!(p::Palette) + _ACTIVE_PALETTE[] = p + return p +end + +""" +$(TYPEDSIGNATURES) + +Override a single semantic role in the active palette and return the updated +[`CTBase.Core.Palette`](@ref). + +`role` must be one of `:name`, `:type`, `:value`, `:keyword`, `:count`, +`:label`, `:emphasis`, `:muted`, `:error`, `:warning`, `:success`. +`code` is the ANSI numeric code string (e.g. `"32"` for green, `"1;34"` for +bold blue, `""` to suppress styling for that role). + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.set_color!(:error, "35") # make errors magenta + +julia> CTBase.Core.reset_palette!() +``` + +See also: [`CTBase.Core.set_palette!`](@ref), [`CTBase.Core.reset_palette!`](@ref) +""" +function set_color!(role::Symbol, code::AbstractString) + p = _ACTIVE_PALETTE[] + new_style = Style(code) + new_palette = Palette( + role == :name ? new_style : p.name, + role == :type ? new_style : p.type, + role == :value ? new_style : p.value, + role == :keyword ? new_style : p.keyword, + role == :count ? new_style : p.count, + role == :label ? new_style : p.label, + role == :emphasis ? new_style : p.emphasis, + role == :muted ? new_style : p.muted, + role == :error ? new_style : p.error, + role == :warning ? new_style : p.warning, + role == :success ? new_style : p.success, + ) + _ACTIVE_PALETTE[] = new_palette + return new_palette +end + +""" +$(TYPEDSIGNATURES) + +Restore the active palette to [`CTBase.Core.DEFAULT_PALETTE`](@ref) and return it. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.set_palette!(CTBase.Core.MONOCHROME_PALETTE) + +julia> CTBase.Core.reset_palette!() + +julia> CTBase.Core.current_palette() === CTBase.Core.DEFAULT_PALETTE +true +``` + +See also: [`CTBase.Core.set_palette!`](@ref), [`CTBase.Core.current_palette`](@ref) +""" +reset_palette!() = set_palette!(DEFAULT_PALETTE) + +# --------------------------------------------------------------------------- +# Palette preview +# --------------------------------------------------------------------------- + +""" +$(TYPEDSIGNATURES) + +Print a visual preview of the active [`CTBase.Core.Palette`](@ref) to `io`. + +The preview has three sections: +- **Role swatches**: every semantic role with a colored swatch block (`████`), a + representative sample string, and a description of when the role is used. +- **Mock describe**: a simulated `describe`/`show` block exercising `name`, `type`, + `value`, `keyword`, `count`, `label`, and `muted`. +- **Mock error**: a simulated exception block exercising `error`, `emphasis`, + `muted`, `warning`, and `success`. + +By default `io` wraps `stdout` with `:color => true` so the preview is always +colored in an interactive session. Pass a custom `IOContext` to override. + +# Example + +```julia-repl +julia> using CTBase + +julia> CTBase.Core.show_palette() + +julia> CTBase.Core.set_palette!(CTBase.Core.HIGH_CONTRAST_PALETTE) +julia> CTBase.Core.show_palette() +julia> CTBase.Core.reset_palette!() +``` + +See also: [`CTBase.Core.set_palette!`](@ref), [`CTBase.Core.current_palette`](@ref), [`CTBase.Core.DEFAULT_PALETTE`](@ref) +""" +function show_palette(io::IO=IOContext(stdout, :color => true)) + p = current_palette() + fmt = ( + bold=get(io, :color, false) ? "\033[1m" : "", + reset=get(io, :color, false) ? "\033[0m" : "", + ) + open(st) = get(io, :color, false) && !isempty(st.code) ? "\033[$(st.code)m" : "" + swatch(st) = isempty(open(st)) ? " " : "$(open(st))████$(fmt.reset)" + rst = fmt.reset + + # Palette name + palette_name = if p === DEFAULT_PALETTE + "DEFAULT_PALETTE" + elseif p === MONOCHROME_PALETTE + "MONOCHROME_PALETTE" + elseif p === HIGH_CONTRAST_PALETTE + "HIGH_CONTRAST_PALETTE" + else + "custom" + end + println(io, fmt.bold, "Active palette: ", palette_name, rst) + println(io) + + # ── Role swatches ────────────────────────────────────────────────────── + println(io, fmt.bold, "Semantic roles", rst) + println(io) + role_rows = [ + (:name, "MyStrategy", "identifiers, type names, option keys"), + (:type, "AbstractStrategy", "type annotations, hierarchy entries"), + (:value, "0.01", "data / option values"), + (:keyword, ":gradient", "Julia symbols, aliases, IDs"), + (:count, "3", "numeric counts, lengths"), + (:label, "[default]", "secondary labels, metadata tags"), + (:emphasis, "important text", "message text, function names"), + (:muted, "│ └─ → (0.5s)", "structural chars, time suffix"), + (:error, "IncorrectArgument", "failures, missing extensions"), + (:warning, "got: -0.5", "notable values, skipped tests"), + (:success, "hint: use set_color!", "positive info, passing tests"), + ] + for (role, sample, desc) in role_rows + st = getfield(p, role) + name = rpad(string(role), 10) + print(io, " $(open(p.muted))$(name)$(rst) $(swatch(st)) ") + println(io, "$(open(st))$(sample)$(rst) $(open(p.muted))$(desc)$(rst)") + end + + # ── Mock describe ────────────────────────────────────────────────────── + println(io) + println(io, fmt.bold, "Mock describe / show", rst) + println(io) + n = open(p.name); t = open(p.type) + v = open(p.value); kw = open(p.keyword) + c = open(p.count); lb = open(p.label) + mu = open(p.muted) + println(io, " $(n)MyStrategy$(rst) (instance, id=$(kw):gradient$(rst))") + println(io, " $(mu)│$(rst)") + println(io, " $(mu)│$(rst) $(lb)hierarchy: $(rst)$(t)MyStrategy$(rst) $(mu)→$(rst) $(t)AbstractStrategy$(rst)") + println(io, " $(mu)│$(rst) $(lb)options ($(rst)$(c)3$(rst)$(lb)):$(rst)") + println(io, " $(mu)│$(rst)") + println(io, " $(mu)│ ├─$(rst) $(n)step$(rst)::$(t)Float64$(rst) = $(v)0.01$(rst) $(lb)[default]$(rst)") + println(io, " $(mu)│ ├─$(rst) $(n)tol$(rst) ::$(t)Float64$(rst) = $(v)1e-6$(rst) $(lb)[user]$(rst)") + println(io, " $(mu)│ └─$(rst) $(n)verbose$(rst)::$(t)Bool$(rst) = $(v)false$(rst) $(lb)[computed]$(rst)") + + # ── Mock error ───────────────────────────────────────────────────────── + println(io) + println(io, fmt.bold, "Mock exception", rst) + println(io) + er = open(p.error); em = open(p.emphasis) + wa = open(p.warning); su = open(p.success) + println(io, " $(er)IncorrectArgument$(rst) $(mu)→$(rst) $(em)my_solver$(rst), $(kw)script.jl:42$(rst)") + println(io, " $(mu)│$(rst)") + println(io, " $(mu)│$(rst) $(em)Invalid value for option :step$(rst)") + println(io, " $(mu)│$(rst)") + println(io, " $(mu)│$(rst) $(em)Got $(rst) $(wa)-0.5$(rst)") + println(io, " $(mu)│$(rst) $(em)Expected$(rst) $(su)> 0.0$(rst)") + println(io, " $(mu)│$(rst)") + println(io, " $(mu)│$(rst) $(em)Hint $(rst) $(su)Use a positive step size for :step$(rst)") + println(io, " $(mu)└─$(rst)") + return nothing +end diff --git a/src/Exceptions/display.jl b/src/Exceptions/display.jl index 80f8dc68..df2c21e2 100644 --- a/src/Exceptions/display.jl +++ b/src/Exceptions/display.jl @@ -309,9 +309,9 @@ Print colored text based on a color symbol. """ function _print_colored(io, text, color::Symbol) if color == :yellow - print(io, Core._yellow(text, io)) + print(io, Core._yellow(text, io)) # warning role elseif color == :green - print(io, Core._green(text, io)) + print(io, Core._green(text, io)) # success role else print(io, text) end diff --git a/src/Strategies/api/describe_registry.jl b/src/Strategies/api/describe_registry.jl index 6b088bae..7eba821e 100644 --- a/src/Strategies/api/describe_registry.jl +++ b/src/Strategies/api/describe_registry.jl @@ -541,10 +541,7 @@ function _describe_single_metadata(io::IO, fmt, strategy_type::Type) fmt.label, "options: ", fmt.reset, - "\033[31m", # Red color - "requires extension ", - ext_names, - "\033[0m", # Reset color + Core._red("requires extension $(ext_names)", io), ) return nothing else @@ -621,10 +618,7 @@ function _describe_multi_param_metadata(io::IO, fmt, strategy_types::Vector, par fmt.label, "options: ", fmt.reset, - "\033[31m", # Red color - "requires extension ", - ext_names, - "\033[0m", # Reset color + Core._red("requires extension $(ext_names)", io), ) return nothing end @@ -681,10 +675,7 @@ function _describe_multi_param_metadata(io::IO, fmt, strategy_types::Vector, par nameof(P), fmt.reset, ": ", - "\033[31m", # Red color - "requires extension ", - ext_names, - "\033[0m", # Reset color + Core._red("requires extension $(ext_names)", io), ) if !is_last_param || !isempty(common_options) println(io, "│") diff --git a/test/suite/core/test_core_display.jl b/test/suite/core/test_core_display.jl index 184bcba7..407f60ec 100644 --- a/test/suite/core/test_core_display.jl +++ b/test/suite/core/test_core_display.jl @@ -53,16 +53,22 @@ function test_core_display() end Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "get_format_codes" begin + all_fields = ( + :bold, :dim, :reset, + :name, :type, :value, :keyword, :count, :label, + :emphasis, :muted, :error, :warning, :success, + ) + 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) + for field in all_fields 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) + for field in all_fields Test.@test fmt[field] != "" end end @@ -70,14 +76,15 @@ function test_core_display() 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) + for field in all_fields + Test.@test haskey(fmt, field) + end + end + + Test.@testset "legacy aliases match semantic fields" begin + fmt = Core.get_format_codes(io_color) + Test.@test fmt.bold == fmt.emphasis + Test.@test fmt.dim == fmt.muted end end diff --git a/test/suite/core/test_palette.jl b/test/suite/core/test_palette.jl new file mode 100644 index 00000000..44bcb4c2 --- /dev/null +++ b/test/suite/core/test_palette.jl @@ -0,0 +1,259 @@ +module TestPalette + +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_palette() + io_plain = IOContext(devnull, :color => false) + io_color = IOContext(devnull, :color => true) + + # Always restore the default palette after this test suite so other tests + # are not affected by palette mutations. + try + _run_palette_tests(io_plain, io_color) + finally + Core.reset_palette!() + end + + return nothing +end + +function _run_palette_tests(io_plain, io_color) + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Style" begin + Test.@testset "construction" begin + s = Core.Style("32") + Test.@test s.code == "32" + s_empty = Core.Style("") + Test.@test s_empty.code == "" + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "Palette" begin + Test.@testset "DEFAULT_PALETTE fields" begin + p = Core.DEFAULT_PALETTE + Test.@test p isa Core.Palette + Test.@test p.name.code == "1;34" + Test.@test p.type.code == "36" + Test.@test p.value.code == "32" + Test.@test p.keyword.code == "33" + Test.@test p.count.code == "35" + Test.@test p.label.code == "90" + Test.@test p.emphasis.code == "1" + Test.@test p.muted.code == "2" + Test.@test p.error.code == "31" + Test.@test p.warning.code == "33" + Test.@test p.success.code == "32" + end + + Test.@testset "MONOCHROME_PALETTE — all codes empty" begin + p = Core.MONOCHROME_PALETTE + for field in fieldnames(Core.Palette) + Test.@test getfield(p, field).code == "" + end + end + + Test.@testset "HIGH_CONTRAST_PALETTE — all codes non-empty" begin + p = Core.HIGH_CONTRAST_PALETTE + for field in fieldnames(Core.Palette) + Test.@test !isempty(getfield(p, field).code) + end + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "current_palette / set_palette!" begin + Test.@testset "default is DEFAULT_PALETTE" begin + Test.@test Core.current_palette() === Core.DEFAULT_PALETTE + end + + Test.@testset "set_palette! changes active palette" begin + Core.set_palette!(Core.MONOCHROME_PALETTE) + Test.@test Core.current_palette() === Core.MONOCHROME_PALETTE + end + + Test.@testset "reset_palette! restores default" begin + Core.set_palette!(Core.HIGH_CONTRAST_PALETTE) + Core.reset_palette!() + Test.@test Core.current_palette() === Core.DEFAULT_PALETTE + end + + Test.@testset "set_palette! returns the new palette" begin + result = Core.set_palette!(Core.MONOCHROME_PALETTE) + Test.@test result === Core.MONOCHROME_PALETTE + Core.reset_palette!() + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "set_color!" begin + Test.@testset "overrides a single role" begin + Core.reset_palette!() + Core.set_color!(:error, "35") + p = Core.current_palette() + Test.@test p.error.code == "35" + # other roles unchanged + Test.@test p.name.code == "1;34" + Test.@test p.success.code == "32" + Core.reset_palette!() + end + + Test.@testset "all roles are settable" begin + roles = (:name, :type, :value, :keyword, :count, :label, + :emphasis, :muted, :error, :warning, :success) + Core.reset_palette!() + for role in roles + Core.set_color!(role, "99") + p = Core.current_palette() + Test.@test getfield(p, role).code == "99" + Core.reset_palette!() + end + end + + Test.@testset "empty code suppresses styling for that role" begin + Core.set_color!(:error, "") + Test.@test Core._red("x", io_color) == "x" + Core.reset_palette!() + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "_style helper" begin + Test.@testset "no color IO — returns plain string" begin + st = Core.Style("32") + Test.@test Core._style(st, "hello", io_plain) == "hello" + end + + Test.@testset "color IO + non-empty code — wraps with escape" begin + st = Core.Style("32") + result = Core._style(st, "hello", io_color) + Test.@test startswith(result, "\033[32m") + Test.@test endswith(result, "\033[0m") + Test.@test contains(result, "hello") + end + + Test.@testset "color IO + empty code — returns plain string" begin + st = Core.Style("") + Test.@test Core._style(st, "hello", io_color) == "hello" + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "MONOCHROME_PALETTE silences all output" begin + Core.set_palette!(Core.MONOCHROME_PALETTE) + + Test.@testset "get_format_codes — all style fields empty" begin + fmt = Core.get_format_codes(io_color) + # :reset is palette-independent (it's "\033[0m" whenever color is on) + # so we only check the style-bearing fields here + for field in ( + :name, :type, :value, :keyword, :count, :label, + :emphasis, :muted, :error, :warning, :success, + :bold, :dim, + ) + Test.@test fmt[field] == "" + end + end + + Test.@testset "named helpers — return plain text" begin + for f in (Core._dim, Core._bold, Core._red, Core._yellow, Core._green) + Test.@test f("x", io_color) == "x" + end + end + + Core.reset_palette!() + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "HIGH_CONTRAST_PALETTE changes codes" begin + Core.set_palette!(Core.HIGH_CONTRAST_PALETTE) + fmt_hc = Core.get_format_codes(io_color) + Core.reset_palette!() + fmt_def = Core.get_format_codes(io_color) + + Test.@testset "error code differs from default" begin + Test.@test fmt_hc.error != fmt_def.error + end + Test.@testset "success code differs from default" begin + Test.@test fmt_hc.success != fmt_def.success + end + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "get_format_codes — new semantic keys present" begin + fmt = Core.get_format_codes(io_color) + for key in (:emphasis, :muted, :error, :warning, :success) + Test.@test haskey(fmt, key) + Test.@test fmt[key] != "" + end + # Legacy aliases still present + Test.@test haskey(fmt, :bold) + Test.@test haskey(fmt, :dim) + Test.@test fmt.bold == fmt.emphasis + Test.@test fmt.dim == fmt.muted + end + + Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "show_palette" begin + Test.@testset "runs without error — default palette" begin + buf = IOBuffer() + io = IOContext(buf, :color => true) + Core.show_palette(io) + output = String(take!(buf)) + Test.@test contains(output, "DEFAULT_PALETTE") + Test.@test contains(output, "Semantic roles") + Test.@test contains(output, "Mock describe") + Test.@test contains(output, "Mock exception") + # All 11 role names appear in the role swatches + for role in ("name", "type", "value", "keyword", "count", + "label", "emphasis", "muted", "error", "warning", "success") + Test.@test contains(output, role) + end + end + + Test.@testset "names active palette — MONOCHROME" begin + Core.set_palette!(Core.MONOCHROME_PALETTE) + buf = IOBuffer() + Core.show_palette(IOContext(buf, :color => true)) + output = String(take!(buf)) + Test.@test contains(output, "MONOCHROME_PALETTE") + Core.reset_palette!() + end + + Test.@testset "names active palette — HIGH_CONTRAST" begin + Core.set_palette!(Core.HIGH_CONTRAST_PALETTE) + buf = IOBuffer() + Core.show_palette(IOContext(buf, :color => true)) + output = String(take!(buf)) + Test.@test contains(output, "HIGH_CONTRAST_PALETTE") + Core.reset_palette!() + end + + Test.@testset "names custom palette as 'custom'" begin + my = Core.Palette( + Core.Style("35"), Core.Style("35"), Core.Style("35"), Core.Style("35"), + Core.Style("35"), Core.Style("35"), Core.Style("35"), Core.Style("35"), + Core.Style("35"), Core.Style("35"), Core.Style("35"), + ) + Core.set_palette!(my) + buf = IOBuffer() + Core.show_palette(IOContext(buf, :color => true)) + output = String(take!(buf)) + Test.@test contains(output, "custom") + Core.reset_palette!() + end + + Test.@testset "plain IO — no ANSI codes in role content" begin + buf = IOBuffer() + io_plain = IOContext(buf, :color => false) + Core.show_palette(io_plain) + output = String(take!(buf)) + # With color disabled the role descriptions must still appear + Test.@test contains(output, "Semantic roles") + # No ANSI escape in content produced by the palette (bold headers may still use IO color) + # The role swatch open() calls return "" when color=false + Test.@test !occursin("\033[1;34m", output) # name role code absent + Test.@test !occursin("\033[31m", output) # error role code absent + end + end + +end + +end # module + +test_palette() = TestPalette.test_palette() diff --git a/test/suite/extensions/test_testrunner_progress.jl b/test/suite/extensions/test_testrunner_progress.jl index d9a2d619..81dcee0e 100644 --- a/test/suite/extensions/test_testrunner_progress.jl +++ b/test/suite/extensions/test_testrunner_progress.jl @@ -187,7 +187,9 @@ function test_testrunner_progress() :final, "/tmp/final.jl", :final, 5, 5, :post_eval, nothing, 0.5 ) buf = IOBuffer() - fmt(buf, info; history=history, cumulative_severity=maximum(history)) + # Use a color-enabled IOContext so ANSI codes are actually emitted + io_c = IOContext(buf, :color => true) + fmt(io_c, info; history=history, cumulative_severity=maximum(history)) output = String(take!(buf)) Test.@test contains(output, "[") Test.@test occursin("\e[31m[", output) # brackets red because of failure @@ -207,7 +209,9 @@ function test_testrunner_progress() 0.2, ) buf = IOBuffer() - fmt(buf, info; cumulative_severity=2) + # Use a color-enabled IOContext so ANSI codes are actually emitted + io_c = IOContext(buf, :color => true) + fmt(io_c, info; cumulative_severity=2) output = String(take!(buf)) Test.@test occursin("\e[33m[", output) || occursin("\e[33m[", replace(output, "\e[0m"=>"")) @@ -240,6 +244,23 @@ function test_testrunner_progress() Test.@test contains(output, "test_example") Test.@test contains(output, "(1.2s)") end + + Test.@testset "palette IO-awareness — no ANSI in plain IO buffer" begin + info = TestRunner.TestRunInfo( + :test_example, + "/tmp/test.jl", + :test_example, + 5, 10, + :post_eval, + nothing, + 1.23, + ) + buf = IOBuffer() + # Plain IOBuffer without :color => true must produce zero ANSI codes + TestRunner._format_progress_line(buf, info; show_progress_bar=false) + output = String(take!(buf)) + Test.@test !occursin("\e[", output) && !occursin("\033[", output) + end end return nothing