From eaea32ee9feb4cdb3538bf0bbaac7c96d8a954cf Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Tue, 21 Oct 2025 16:17:25 +0200 Subject: [PATCH] Stream_support: add indenting and coloring stream wrappers --- .../examples/Stream_support/CMakeLists.txt | 4 + .../examples/Stream_support/color_ostream.cpp | 365 +++++++++ .../Stream_support/indenting_ostream.cpp | 167 +++++ .../include/CGAL/IO/Color_ostream.h | 693 ++++++++++++++++++ .../include/CGAL/IO/Indenting_ostream.h | 330 +++++++++ 5 files changed, 1559 insertions(+) create mode 100644 Stream_support/examples/Stream_support/color_ostream.cpp create mode 100644 Stream_support/examples/Stream_support/indenting_ostream.cpp create mode 100644 Stream_support/include/CGAL/IO/Color_ostream.h create mode 100644 Stream_support/include/CGAL/IO/Indenting_ostream.h diff --git a/Stream_support/examples/Stream_support/CMakeLists.txt b/Stream_support/examples/Stream_support/CMakeLists.txt index fc5a12d6f6e..16f53717bb6 100644 --- a/Stream_support/examples/Stream_support/CMakeLists.txt +++ b/Stream_support/examples/Stream_support/CMakeLists.txt @@ -18,3 +18,7 @@ create_single_source_cgal_program( "off2wav.cpp" ) create_single_source_cgal_program( "off_bbox.cpp" ) create_single_source_cgal_program( "off_glue.cpp" ) create_single_source_cgal_program( "off_transform.cpp" ) + +create_single_source_cgal_program( "indenting_ostream.cpp" ) +create_single_source_cgal_program( "color_ostream.cpp" ) + diff --git a/Stream_support/examples/Stream_support/color_ostream.cpp b/Stream_support/examples/Stream_support/color_ostream.cpp new file mode 100644 index 00000000000..e92c9b0dbc1 --- /dev/null +++ b/Stream_support/examples/Stream_support/color_ostream.cpp @@ -0,0 +1,365 @@ +#include +#include + +/*! + * \example Stream_support/color_ostream.cpp + * + * This example demonstrates how to use CGAL::IO::Basic_color_streambuf + * and CGAL::IO::Basic_color_stream_guard for formatting output with + * ANSI colors. + */ + +using CGAL::IO::Ansi_color; + +void demonstrate_basic_colors() { + std::cout << "=== Basic Color Usage ===\n\n"; + + // Example 1: Single color + { + std::cout << "1. Single color (Red):\n"; + CGAL::IO::Color_stream_guard guard(std::cout, Ansi_color::Red); + + std::cout << "This text is red\n"; + std::cout << "All lines in this scope are red\n"; + } + std::cout << "Back to normal color\n\n"; + + // Example 2: Different colors + { + std::cout << "2. Different colors:\n"; + + { + CGAL::IO::Color_stream_guard green(std::cout, Ansi_color::Green); + std::cout << "Green text\n"; + } + + { + CGAL::IO::Color_stream_guard blue(std::cout, Ansi_color::Blue); + std::cout << "Blue text\n"; + } + + { + CGAL::IO::Color_stream_guard yellow(std::cout, Ansi_color::Yellow); + std::cout << "Yellow text\n"; + } + } + std::cout << "Back to normal\n\n"; + + // Example 3: Bright colors + { + std::cout << "3. Bright colors:\n"; + + { + CGAL::IO::Color_stream_guard bright_red(std::cout, Ansi_color::BrightRed); + std::cout << "Bright red text\n"; + } + + { + CGAL::IO::Color_stream_guard bright_green(std::cout, Ansi_color::BrightGreen); + std::cout << "Bright green text\n"; + } + + { + CGAL::IO::Color_stream_guard bright_cyan(std::cout, Ansi_color::BrightCyan); + std::cout << "Bright cyan text\n"; + } + } + std::cout << "Back to normal\n\n"; +} + +void demonstrate_combined_colors() { + std::cout << "=== Combined Colors (Bold, Underline, etc.) ===\n\n"; + + // Example 1: Bold text + { + std::cout << "1. Bold colors:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Red}); + + std::cout << "This text is bold and red\n"; + } + std::cout << "Back to normal\n\n"; + + // Example 2: Underlined text + { + std::cout << "2. Underlined colors:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Underline, Ansi_color::Blue}); + + std::cout << "This text is underlined and blue\n"; + } + std::cout << "Back to normal\n\n"; + + // Example 3: Bold + underline + color + { + std::cout << "3. Multiple attributes:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Underline, Ansi_color::Green}); + + std::cout << "This text is bold, underlined, and green\n"; + } + std::cout << "Back to normal\n\n"; +} + +void demonstrate_background_colors() { + std::cout << "=== Background Colors ===\n\n"; + + { + std::cout << "1. Red background:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, Ansi_color::BgRed); + std::cout << "Text with red background\n"; + } + std::cout << "Back to normal\n\n"; + + { + std::cout << "2. Foreground + background:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Yellow, Ansi_color::BgBlue}); + std::cout << "Yellow text on blue background\n"; + } + std::cout << "Back to normal\n\n"; + + { + std::cout << "3. Bold white on bright green background:\n"; + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::White, Ansi_color::BgBrightGreen}); + std::cout << "Bold white on bright green\n"; + } + std::cout << "Back to normal\n\n"; +} + +void demonstrate_nested_colors() { + std::cout << "=== Nested Color Scopes ===\n\n"; + + { + CGAL::IO::Color_stream_guard outer(std::cout, Ansi_color::Blue); + std::cout << "Outer scope: blue text\n"; + + { + CGAL::IO::Color_stream_guard inner(std::cout, Ansi_color::Red); + std::cout << "Inner scope: red text\n"; + + { + CGAL::IO::Color_stream_guard innermost(std::cout, Ansi_color::Green); + std::cout << "Innermost scope: green text\n"; + } + + std::cout << "Back to inner scope: red text\n"; + } + + std::cout << "Back to outer scope: blue text\n"; + } + std::cout << "Back to normal\n\n"; +} + +void simulate_colored_debug_output() { + std::cout << "=== Simulated Colored Debug Output ===\n\n"; + + std::cout << "Algorithm: Starting computation\n"; + + { + CGAL::IO::Color_stream_guard info(std::cout, Ansi_color::Cyan); + std::cout << "INFO: Initializing data structures\n"; + } + + { + CGAL::IO::Color_stream_guard success(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Green}); + std::cout << "SUCCESS: Data loaded successfully\n"; + } + + { + CGAL::IO::Color_stream_guard warning(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Yellow}); + std::cout << "WARNING: Large dataset detected, may take longer\n"; + } + + { + CGAL::IO::Color_stream_guard processing(std::cout, Ansi_color::Blue); + std::cout << "Processing vertices...\n"; + std::cout << "Processing edges...\n"; + std::cout << "Processing faces...\n"; + } + + { + CGAL::IO::Color_stream_guard success(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Green}); + std::cout << "SUCCESS: Computation completed\n"; + } + + std::cout << "Algorithm finished\n\n"; +} + +void demonstrate_multiple_streams() { + std::cout << "=== Multiple Streams with make_color_guards ===\n\n"; + + std::cout << "Before coloring:\n"; + std::cout << " cout: Normal output\n"; + std::cerr << " cerr: Normal output\n"; + + // Apply colors to both streams simultaneously + { + std::cout << "\nWith make_color_guards (Red):\n"; + auto guards = CGAL::IO::make_color_guards(Ansi_color::Red, std::cout, std::cerr); + + std::cout << "Red text on cout\n"; + std::cerr << "Red text on cerr\n"; + } + + std::cout << "\nAfter color scope:\n"; + std::cout << " cout: Normal output again\n"; + std::cerr << " cerr: Normal output again\n"; + + // Using combined colors with multiple streams + { + std::cout << "\nBold green on multiple streams:\n"; + auto guards = CGAL::IO::make_color_guards( + std::vector{Ansi_color::Bold, Ansi_color::Green}, + std::cout, std::clog); + + std::cout << "cout: Bold green output\n"; + std::clog << "clog: Bold green log message\n"; + } + + std::cout << "\n"; +} + +void test_color_detection() { + std::cout << "=== Color Detection Tests ===\n\n"; + + // Test automatic detection in streambuf + std::cout << "1. Testing automatic color detection in Basic_color_streambuf:\n"; + { + CGAL::IO::Color_streambuf color_buf(*std::cout.rdbuf(), Ansi_color::Green); + std::cout << " Colors automatically detected as: " << (color_buf.colors_enabled() ? "ENABLED" : "DISABLED") + << "\n"; + + // Temporarily install the buffer to show it works + auto* old_buf = std::cout.rdbuf(&color_buf); + std::cout << " (This text should be green only if colors were auto-detected)\n"; + std::cout.rdbuf(old_buf); + } + std::cout << "\n"; + + // Test stdout color support function + std::cout << "2. Testing stdout_supports_color() function:\n"; + if(CGAL::IO::stdout_supports_color()) { + std::cout << " stdout supports colors: YES\n"; + { + CGAL::IO::Color_stream_guard guard(std::cout, Ansi_color::Green); + std::cout << " (This text should be green if color is truly supported)\n"; + } + } else { + std::cout << " stdout supports colors: NO\n"; + std::cout << " (Colors are disabled - may be due to NO_COLOR env var,\n"; + std::cout << " redirection to file, or unsupported terminal)\n"; + } + std::cout << "\n"; + + // Test stderr color support + std::cout << "3. Testing stderr_supports_color():\n"; + if(CGAL::IO::stderr_supports_color()) { + std::cout << " stderr supports colors: YES\n"; + { + CGAL::IO::Color_stream_guard guard(std::cerr, Ansi_color::Yellow); + std::cerr << " (This text should be yellow if color is truly supported)\n"; + } + } else { + std::cout << " stderr supports colors: NO\n"; + } + std::cout << "\n"; + + // Test stream_supports_color() with different streams + std::cout << "4. Testing stream_supports_color() for different streams:\n"; + std::cout << " std::cout:" << (CGAL::IO::stream_supports_color(std::cout) ? "YES" : "NO") << "\n"; + std::cout << " std::cerr:" << (CGAL::IO::stream_supports_color(std::cerr) ? "YES" : "NO") << "\n"; + std::cout << " std::clog: " << (CGAL::IO::stream_supports_color(std::clog) ? "YES" : "NO") << "\n"; + std::cout << "\n"; + + // Demonstrate automatic coloring (no manual check needed!) + std::cout << "5. Automatic coloring example (no manual checking!):\n"; + { + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Cyan}); + std::cout << " This message is automatically colored only if terminal supports it!\n"; + std::cout << " No need to check stream_supports_color() manually - it's automatic!\n"; + } + std::cout << "\n"; + + // Demonstrate smart error/warning coloring + std::cout << "6. Smart colored output (automatically adapts to terminal capabilities):\n"; + + // Info message - colors applied automatically! + std::cout << " "; + { + CGAL::IO::Color_stream_guard guard(std::cout, Ansi_color::Cyan); + std::cout << "INFO:"; + } + std::cout << " Normal information message\n"; + + // Success message - colors applied automatically! + std::cout << " "; + { + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Green}); + std::cout << "SUCCESS:"; + } + std::cout << " Operation completed successfully\n"; + + // Warning message - colors applied automatically! + std::cout << " "; + { + CGAL::IO::Color_stream_guard guard(std::cout, + std::vector{Ansi_color::Bold, Ansi_color::Yellow}); + std::cout << "WARNING:"; + } + std::cout << " Potential issue detected\n"; + + // Error message (using cerr) - colors applied automatically! + std::cerr << " "; + { + CGAL::IO::Color_stream_guard guard(std::cerr, + std::vector{Ansi_color::Bold, Ansi_color::Red}); + std::cerr << "ERROR:"; + } + std::cerr << " Critical error occurred\n"; + std::cout << "\n"; + + // Environment hints + std::cout << "7. Environment information:\n"; + const char* term = std::getenv("TERM"); + const char* no_color = std::getenv("NO_COLOR"); + + std::cout << " TERM variable: " << (term ? term : "(not set)") << "\n"; + std::cout << " NO_COLOR variable: " << (no_color ? "SET (colors disabled)" : "(not set)") << "\n"; + +#ifdef _WIN32 + std::cout << " Platform: Windows\n"; + const char* ansicon = std::getenv("ANSICON"); + std::cout << " ANSICON variable: " << (ansicon ? ansicon : "(not set)") << "\n"; +#else + std::cout << " Platform: POSIX (Linux/macOS/Unix)\n"; +#endif + + std::cout << "\n"; + std::cout << " Tip: To disable colors, set the NO_COLOR environment variable:\n"; + std::cout << " export NO_COLOR=1\n"; + std::cout << " Tip: When redirecting to a file, colors are automatically disabled:\n"; + std::cout << " ./color_ostream > output.txt\n"; + std::cout << "\n"; +} + +int main() { + // First, test color detection + test_color_detection(); + + // Then demonstrate color features + demonstrate_basic_colors(); + demonstrate_combined_colors(); + demonstrate_background_colors(); + demonstrate_nested_colors(); + simulate_colored_debug_output(); + demonstrate_multiple_streams(); + + return 0; +} diff --git a/Stream_support/examples/Stream_support/indenting_ostream.cpp b/Stream_support/examples/Stream_support/indenting_ostream.cpp new file mode 100644 index 00000000000..b2519f6ece3 --- /dev/null +++ b/Stream_support/examples/Stream_support/indenting_ostream.cpp @@ -0,0 +1,167 @@ +#include +#include +#include + +/*! + * \example Stream_support/indenting_ostream.cpp + * + * This example demonstrates how to use CGAL::IO::Basic_indenting_streambuf + * and CGAL::IO::basic_indenting_stream_guard for formatting output with + * automatic indentation. + */ + +void demonstrate_basic_usage() { + std::cout << "=== Basic Indenting Usage ===\n\n"; + + // Example 1: Basic indentation with std::cout + { + std::cout << "1. Basic indentation with std::cout:\n"; + CGAL::IO::Indenting_stream_guard guard(std::cout, " "); + + std::cout << "This line is indented\n"; + std::cout << "So is this one\n"; + std::cout << "Multi-line output:\n"; + std::cout << "Line 1\nLine 2\nLine 3\n"; + } + std::cout << "Back to normal indentation\n\n"; + + // Example 2: Nested indentation levels + { + std::cout << "2. Nested indentation levels:\n"; + CGAL::IO::Indenting_stream_guard level1(std::cout, "| "); + + std::cout << "Level 1 indentation\n"; + { + CGAL::IO::Indenting_stream_guard level2(std::cout, "| "); + std::cout << "Level 2 indentation\n"; + { + CGAL::IO::Indenting_stream_guard level3(std::cout, "| "); + std::cout << "Level 3 indentation\n"; + } + std::cout << "Back to level 2\n"; + } + std::cout << "Back to level 1\n"; + } + std::cout << "Back to normal\n\n"; + + // Example 3: Custom indentation string + { + std::cout << "3. Custom indentation string:\n"; + CGAL::IO::Indenting_stream_guard guard(std::cout, ">>> "); + + std::cout << "Custom prefix on each line\n"; + std::cout << "Works with multiple lines too\n"; + } + std::cout << "Normal output again\n\n"; +} + +void demonstrate_stringstream_usage() { + std::cout << "=== Stringstream Usage ===\n\n"; + + // Using with stringstream for formatted output + // Note: We create a custom ostream using the indenting streambuf + std::ostringstream backing_stream; + CGAL::IO::Indenting_streambuf indent_buf(*backing_stream.rdbuf(), " "); + std::ostream oss(&indent_buf); + + oss << "This goes to stringstream\n"; + oss << "With indentation\n"; + oss << "Multiple lines\n"; + + std::cout << "Stringstream contents:\n" << backing_stream.str() << "\n"; +} + +void simulate_debug_output() { + std::cout << "=== Simulated Debug Output ===\n\n"; + + std::cout << "Algorithm: Starting geometric computation\n"; + + { + CGAL::IO::Indenting_stream_guard guard(std::cout, " "); + std::cout << "Phase 1: Input validation\n"; + std::cout << "- Checking vertices: OK\n"; + std::cout << "- Checking faces: OK\n"; + + { + CGAL::IO::Indenting_stream_guard inner_guard(std::cout, " "); + std::cout << "Detailed validation:\n"; + std::cout << "- Manifold check: PASSED\n"; + std::cout << "- Orientation check: PASSED\n"; + std::cout << "- Degeneracy check: PASSED\n"; + } + + std::cout << "Phase 1 complete\n\n"; + + std::cout << "Phase 2: Triangulation\n"; + std::cout << "- Inserting vertices...\n"; + std::cout << "- Computing Delaunay triangulation...\n"; + std::cout << "- Applying constraints...\n"; + + { + CGAL::IO::Indenting_stream_guard constraint_guard(std::cout, " "); + std::cout << "Constraint processing:\n"; + for(int i = 0; i < 3; ++i) { + std::cout << "- Processing constraint " << i << ": OK\n"; + } + } + + std::cout << "Phase 2 complete\n"; + } + + std::cout << "Algorithm: Geometric computation finished successfully\n\n"; +} + +void demonstrate_multiple_streams() { + std::cout << "=== Multiple Streams with make_indenting_guards ===\n\n"; + + std::cout << "Before indentation:\n"; + std::cout << " cout: Normal output\n"; + std::cerr << " cerr: Normal output\n"; + + // Apply indentation to both streams simultaneously + { + std::cout << "\nWith make_indenting_guards (2 spaces):\n"; + auto guards = CGAL::IO::make_indenting_guards(2, std::cout, std::cerr); + + std::cout << "This line is indented on cout\n"; + std::cout << "And this one too\n"; + + std::cerr << "This line is indented on cerr\n"; + std::cerr << "And this one too\n"; + + // Nested indentation works too + { + auto nested_guards = CGAL::IO::make_indenting_guards(2, std::cout, std::cerr); + std::cout << "Double indented on cout\n"; + std::cerr << "Double indented on cerr\n"; + } + + std::cout << "Back to single indent\n"; + std::cerr << "Back to single indent\n"; + } + + std::cout << "\nAfter indentation scope:\n"; + std::cout << " cout: Normal output again\n"; + std::cerr << " cerr: Normal output again\n"; + + // Using custom indent string with multiple streams + { + std::cout << "\nUsing custom indent string \">> \" with std::cout and std::clog:\n"; + auto guards = CGAL::IO::make_indenting_guards(">> ", std::cout, std::clog); + + std::cout << "cout: Output with custom indent\n"; + std::clog << "clog: Log message with custom indent\n"; + std::cout << "cout: More output\n"; + } + + std::cout << "\n"; +} + +int main() { + demonstrate_basic_usage(); + demonstrate_stringstream_usage(); + simulate_debug_output(); + demonstrate_multiple_streams(); + + return 0; +} \ No newline at end of file diff --git a/Stream_support/include/CGAL/IO/Color_ostream.h b/Stream_support/include/CGAL/IO/Color_ostream.h new file mode 100644 index 00000000000..013a80edfd4 --- /dev/null +++ b/Stream_support/include/CGAL/IO/Color_ostream.h @@ -0,0 +1,693 @@ +// Copyright (c) 2025 +// Utrecht University (The Netherlands), +// ETH Zurich (Switzerland), +// INRIA Sophia-Antipolis (France), +// Max-Planck-Institute Saarbruecken (Germany), +// and Tel-Aviv University (Israel). All rights reserved. +// +// This file is part of CGAL (www.cgal.org) +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial +// +// +// Author(s) : Laurent Rineau + +#ifndef CGAL_IO_COLOR_OSTREAM_H +#define CGAL_IO_COLOR_OSTREAM_H + +#include + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define CGAL_ISATTY _isatty +#define CGAL_FILENO _fileno +#else +#include +#define CGAL_ISATTY isatty +#define CGAL_FILENO fileno +#endif + +#include // for getenv + +namespace CGAL { +namespace IO { + +/** + * \ingroup PkgStreamSupportRef + * + * ANSI color codes for terminal output. + */ +enum class Ansi_color { + Reset = 0, + Bold = 1, + Dim = 2, + Underline = 4, + Blink = 5, + Reverse = 7, + Hidden = 8, + + // Foreground colors + Black = 30, + Red = 31, + Green = 32, + Yellow = 33, + Blue = 34, + Magenta = 35, + Cyan = 36, + White = 37, + + // Bright foreground colors + BrightBlack = 90, + BrightRed = 91, + BrightGreen = 92, + BrightYellow = 93, + BrightBlue = 94, + BrightMagenta = 95, + BrightCyan = 96, + BrightWhite = 97, + + // Background colors + BgBlack = 40, + BgRed = 41, + BgGreen = 42, + BgYellow = 43, + BgBlue = 44, + BgMagenta = 45, + BgCyan = 46, + BgWhite = 47, + + // Bright background colors + BgBrightBlack = 100, + BgBrightRed = 101, + BgBrightGreen = 102, + BgBrightYellow = 103, + BgBrightBlue = 104, + BgBrightMagenta = 105, + BgBrightCyan = 106, + BgBrightWhite = 107 +}; + +/** + * \ingroup PkgStreamSupportRef + * + * The class template `Basic_color_streambuf` wraps another `basic_streambuf` + * and automatically adds ANSI color codes to the output. This is useful for + * colorizing terminal output with consistent color schemes. + * + * \tparam CharT Character type (typically `char` or `wchar_t`) + * \tparam Traits Character traits type + */ +template > +class Basic_color_streambuf : public std::basic_streambuf, protected Traits +{ +public: + using char_type = CharT; + using traits_type = Traits; + using int_type = typename traits_type::int_type; + using pos_type = typename traits_type::pos_type; + using off_type = typename traits_type::off_type; + using streambuf_type = std::basic_streambuf; + using string = std::basic_string; + +private: + streambuf_type* wrapped_buf_; + string color_code_; + bool at_line_start_; + bool colors_enabled_; + + /** \brief Detect if the wrapped buffer supports color output. */ + static bool detect_color_support(streambuf_type* buf) { + // Check NO_COLOR environment variable first + if(std::getenv("NO_COLOR")) { + return false; + } + + // Try to determine if this is stdout or stderr + // by comparing pointers (works for standard streams) + if(buf == std::cout.rdbuf() || buf == std::clog.rdbuf()) { + int fd = CGAL_FILENO(stdout); + if(!CGAL_ISATTY(fd)) { + return false; + } + } else if(buf == std::cerr.rdbuf()) { + int fd = CGAL_FILENO(stderr); + if(!CGAL_ISATTY(fd)) { + return false; + } + } else { + // Unknown buffer, conservatively disable colors + return false; + } + +#ifdef _WIN32 + // Windows: check for ANSICON or assume modern Windows + return std::getenv("ANSICON") || true; +#else + // POSIX: check TERM environment variable + const char* term = std::getenv("TERM"); + if(!term) { + return false; + } + + std::string term_str(term); + if(term_str == "dumb") { + return false; + } + + // Check for common color-capable terminals + return term_str.find("color") != std::string::npos || + term_str.find("xterm") != std::string::npos || + term_str.find("rxvt") != std::string::npos || + term_str.find("screen") != std::string::npos || + term_str.find("tmux") != std::string::npos || + term_str.find("linux") != std::string::npos; +#endif + } + + /** \brief Generate ANSI escape sequence for a color. */ + static string make_color_code(Ansi_color color) { + string code; + code += char_type('\033'); + code += char_type('['); + int color_value = static_cast(color); + // Convert integer to string + if(color_value == 0) { + code += char_type('0'); + } else { + string digits; + while(color_value > 0) { + digits = char_type('0' + (color_value % 10)) + digits; + color_value /= 10; + } + code += digits; + } + code += char_type('m'); + return code; + } + +public: + /** + * \brief Construct a color streambuf wrapper. + * + * Colors are automatically disabled if the wrapped buffer is not connected + * to a color-capable terminal (checked via isatty() and TERM variable). + * + * \param wrapped_buf The underlying streambuf to wrap + * \param color The color to apply to the output + */ + explicit + Basic_color_streambuf(streambuf_type& wrapped_buf, + Ansi_color color = Ansi_color::Reset) + : wrapped_buf_(&wrapped_buf) + , color_code_(make_color_code(color)) + , at_line_start_(true) + , colors_enabled_(detect_color_support(&wrapped_buf)) + { + } + + /** + * \brief Construct a color streambuf wrapper with multiple colors. + * + * Colors are automatically disabled if the wrapped buffer is not connected + * to a color-capable terminal (checked via isatty() and TERM variable). + * + * \param wrapped_buf The underlying streambuf to wrap + * \param colors Vector of colors to combine (e.g., bold + red) + */ + Basic_color_streambuf(streambuf_type& wrapped_buf, + const std::vector& colors) + : wrapped_buf_(&wrapped_buf) + , at_line_start_(true) + , colors_enabled_(detect_color_support(&wrapped_buf)) + { + if(!colors.empty()) { + color_code_ += char_type('\033'); + color_code_ += char_type('['); + for(std::size_t i = 0; i < colors.size(); ++i) { + if(i > 0) color_code_ += char_type(';'); + int color_value = static_cast(colors[i]); + // Convert integer to string + if(color_value == 0) { + color_code_ += char_type('0'); + } else { + string digits; + while(color_value > 0) { + digits = char_type('0' + (color_value % 10)) + digits; + color_value /= 10; + } + color_code_ += digits; + } + } + color_code_ += char_type('m'); + } + } + + // Non-copyable + Basic_color_streambuf(const Basic_color_streambuf&) = delete; + Basic_color_streambuf& operator=(const Basic_color_streambuf&) = delete; + + // Move constructor + Basic_color_streambuf(Basic_color_streambuf&& other) noexcept + : wrapped_buf_(std::exchange(other.wrapped_buf_, nullptr)) + , color_code_(std::move(other.color_code_)) + , at_line_start_(std::exchange(other.at_line_start_, true)) + , colors_enabled_(std::exchange(other.colors_enabled_, false)) + { + } + + // Move assignment + Basic_color_streambuf& operator=(Basic_color_streambuf&& other) noexcept { + if(this != &other) { + // If currently wrapping a buffer, try to sync it first. + if(wrapped_buf_) { + wrapped_buf_->pubsync(); + } + wrapped_buf_ = std::exchange(other.wrapped_buf_, nullptr); + color_code_ = std::move(other.color_code_); + at_line_start_ = std::exchange(other.at_line_start_, true); + colors_enabled_ = std::exchange(other.colors_enabled_, false); + } + return *this; + } + + ~Basic_color_streambuf() noexcept { + if(wrapped_buf_) { + if(colors_enabled_ && !color_code_.empty() && !at_line_start_) { + string reset_code = make_color_code(Ansi_color::Reset); + (void)put_a_string(reset_code); + } + wrapped_buf_->pubsync(); + } + } + + /** \brief Get the current color code string. */ + const string& color_code() const { return color_code_; } + + /** + * \brief Set a new color code. + * + * \param string The new color code to apply + */ + void set_color_code(const string& color_code) { + color_code_ = color_code; + } + + /** \brief Check if colors are enabled for this streambuf. */ + bool colors_enabled() const { return colors_enabled_; } + + /** \brief Get the wrapped streambuf. */ + streambuf_type& wrapped_streambuf() const { return *wrapped_buf_; } + +protected: + using traits_type::eof; + using traits_type::not_eof; + using traits_type::to_char_type; + using traits_type::to_int_type; + using traits_type::eq_int_type; + + bool put_a_string(const string& str) { + for(char_type ch : str) { + if(eq_int_type(wrapped_buf_->sputc(ch), eof())) { + return false; + } + } + return true; + } + + int_type overflow(int_type ch = eof()) override { + if(eq_int_type(ch, eof())) { + return wrapped_buf_->pubsync() == 0 ? not_eof(ch) : eof(); + } + + // If we're at the start of a line, output the color code first (if colors enabled) + if(colors_enabled_ && at_line_start_ && !color_code_.empty()) { + if(!put_a_string(color_code_)) { + return eof(); + } + at_line_start_ = false; + } + + // Check if this character is a newline + if(to_char_type(ch) == char_type('\n')) { + // Output reset code before newline (if colors enabled) + if(colors_enabled_ && !color_code_.empty()) { + string reset_code = make_color_code(Ansi_color::Reset); + if(!put_a_string(reset_code)) { + return eof(); + } + } + at_line_start_ = true; + } + + // Output the actual character + return wrapped_buf_->sputc(to_char_type(ch)); + } + + int sync() override { return wrapped_buf_->pubsync(); } + + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + std::streamsize written = 0; + + for(std::streamsize i = 0; i < count; ++i) { + if(eq_int_type(overflow(to_int_type(s[i])), eof())) { + break; + } + ++written; + } + + return written; + } + + pos_type seekoff(off_type off, + std::ios_base::seekdir dir, + std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override { + return wrapped_buf_->pubseekoff(off, dir, which); + } + + pos_type seekpos(pos_type pos, std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override { + return wrapped_buf_->pubseekpos(pos, which); + } + + std::streamsize showmanyc() override { + return wrapped_buf_->in_avail(); + } + + std::streamsize xsgetn(char_type* s, std::streamsize count) override { + return wrapped_buf_->sgetn(s, count); + } + + int_type underflow() override { + return wrapped_buf_->sgetc(); + } + + int_type uflow() override { + return wrapped_buf_->sbumpc(); + } + + int_type pbackfail(int_type c = eof()) override { + return wrapped_buf_->sputbackc(to_char_type(c)); + } + + void imbue(const std::locale& loc) override { + wrapped_buf_->pubimbue(loc); + } + + streambuf_type* setbuf(char_type* s, std::streamsize n) override { + return wrapped_buf_->pubsetbuf(s, n); + } +}; + +/// Type alias for `Basic_color_streambuf` +using Color_streambuf = Basic_color_streambuf; + +/// Type alias for `Basic_color_streambuf` +using Color_wstreambuf = Basic_color_streambuf; + +/** + * \ingroup PkgStreamSupportRef + * + * RAII helper class for temporarily installing a color streambuf on a stream. + * + * This class automatically restores the original streambuf when it goes out of scope. + * It provides a convenient way to add colors to output streams within a specific scope. + * + * \tparam StreamT The stream type (e.g., `std::ostream`, `std::wostream`) + */ +template +class Basic_color_stream_guard +{ +private: + using char_type = typename StreamT::char_type; + using traits_type = typename StreamT::traits_type; + using streambuf_type = Basic_color_streambuf; + using wrapped_streambuf_type = std::basic_streambuf; + using string = std::basic_string; + StreamT& stream_; + wrapped_streambuf_type* original_buf_; + string original_color_code_; + streambuf_type color_buf_; + +public: + /** + * \brief Construct and install a color streambuf on the given stream. + * + * \param stream The stream to modify + * \param color The color to apply to the output + */ + explicit Basic_color_stream_guard(StreamT& stream, + Ansi_color color) + : stream_(stream) + , original_buf_(stream.rdbuf()) + , color_buf_(*original_buf_, color) + { + if(auto old_buf = dynamic_cast(original_buf_)) { + original_color_code_ = old_buf->color_code(); + // For nested colors, we keep the current buffer but note the original color + } else { + stream.rdbuf(&color_buf_); + } + } + + /** + * \brief Construct and install a color streambuf with multiple colors. + * + * \param stream The stream to modify + * \param colors Vector of colors to combine (e.g., bold + red) + */ + Basic_color_stream_guard(StreamT& stream, + const std::vector& colors) + : stream_(stream) + , original_buf_(stream.rdbuf()) + , color_buf_(*original_buf_, colors) + { + if(auto old_buf = dynamic_cast(original_buf_)) { + original_color_code_ = old_buf->color_code(); + } else { + stream.rdbuf(&color_buf_); + } + } + + // Non-copyable, movable + Basic_color_stream_guard(const Basic_color_stream_guard&) = delete; + Basic_color_stream_guard& operator=(const Basic_color_stream_guard&) = delete; + + Basic_color_stream_guard(Basic_color_stream_guard&& other) + : stream_(other.stream_) + , original_buf_(other.original_buf_) + , original_color_code_(std::move(other.original_color_code_)) + , color_buf_(std::move(other.color_buf_)) + { + // Update the stream to point to the new buffer location if it was using the old one + if(stream_.rdbuf() == &other.color_buf_) { + stream_.rdbuf(&color_buf_); + } + // Mark other as moved-from so it won't restore the stream + other.original_buf_ = nullptr; + } + + Basic_color_stream_guard& operator=(Basic_color_stream_guard&&) = delete; + + /** \brief Destructor - restores the original streambuf. */ + ~Basic_color_stream_guard() { + if(!original_buf_) return; // Moved-from object + if(auto old_buf = dynamic_cast(original_buf_)) { + old_buf->set_color_code(original_color_code_); + } else { + stream_.rdbuf(original_buf_); + } + } +}; + +/// Type alias for `Basic_color_stream_guard` +using Color_stream_guard = Basic_color_stream_guard; + +/// Type alias for `Basic_color_stream_guard` +using Color_wstream_guard = Basic_color_stream_guard; + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Create color guards for multiple streams simultaneously. + * + * This helper function creates `Basic_color_stream_guard` objects for + * multiple streams at once. All streams will use the same color settings, + * and their original streambufs will be automatically restored when the guards + * go out of scope. The guards are returned in a tuple. + * + * \tparam Streams The stream types (deduced from arguments) + * \param color The color to apply + * \param streams The streams to apply colors to + * \return A tuple of Basic_color_stream_guard objects + * + * Example usage: + * \code + * auto guards = CGAL::make_color_guards(Color::Red, std::cout, std::cerr); + * std::cout << "Red output\n"; + * std::cerr << "Red error\n"; + * \endcode + */ +template +auto make_color_guards(Ansi_color color, Streams&... streams) { + return std::make_tuple(Basic_color_stream_guard(streams, color)...); +} + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Create color guards for multiple streams with multiple colors. + * + * This overload allows specifying multiple colors to combine (e.g., bold + red). + * + * \tparam Streams The stream types (deduced from arguments) + * \param colors Vector of colors to combine + * \param streams The streams to apply colors to + * \return A tuple of Basic_color_stream_guard objects + */ +template +auto make_color_guards(const std::vector& colors, Streams&... streams) { + return std::make_tuple(Basic_color_stream_guard(streams, colors)...); +} + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Check if a stream is attached to a terminal that supports colors. + * + * This function checks if the given output stream is connected to a terminal + * (TTY) that can display ANSI color codes. It performs the following checks: + * + * 1. Verifies the stream is attached to a TTY device using `isatty()` + * 2. Checks if the `TERM` environment variable indicates color support + * 3. Respects the `NO_COLOR` environment variable (if set, colors are disabled) + * 4. On Windows, also checks for `ANSICON` environment variable + * + * \param stream The output stream to check (e.g., `std::cout`, `std::cerr`) + * \return `true` if the stream supports color output, `false` otherwise + * + * \note This function may return false negatives on some systems, but should + * never return false positives (it won't report color support where + * it doesn't exist). + * + * Example usage: + * \code + * if(CGAL::stream_supports_color(std::cout)) { + * CGAL::Color_stream_guard guard(std::cout, CGAL::Ansi_color::Red); + * std::cout << "This text is red!\n"; + * } else { + * std::cout << "Plain text output\n"; + * } + * \endcode + */ +template +bool stream_supports_color(const std::basic_ostream& stream) { + // Check if NO_COLOR environment variable is set (standard way to disable colors) + if(std::getenv("NO_COLOR")) { + return false; + } + + // Try to get the file descriptor for the stream + // This works for std::cout (stdout) and std::cerr (stderr) + const auto* fbuf = dynamic_cast*>(stream.rdbuf()); + if(!fbuf) { + // If it's not a file buffer, check if it's stdout or stderr directly + const std::basic_ostream* std_stream = &stream; + int fd = -1; + + if(std_stream == &std::cout || std_stream == &std::clog) { + fd = CGAL_FILENO(stdout); + } else if(std_stream == &std::cerr) { + fd = CGAL_FILENO(stderr); + } + + if(fd == -1) { + return false; // Unknown stream type + } + + // Check if the file descriptor is a TTY + if(!CGAL_ISATTY(fd)) { + return false; + } + } else { + // For file buffers, we can't easily get the fd in a portable way + // Conservatively assume no color support for files + return false; + } + +#ifdef _WIN32 + // On Windows, check for ANSICON or Windows 10+ with VT100 support + // Modern Windows Terminal and ConEmu support ANSI colors + if(std::getenv("ANSICON")) { + return true; + } + // Windows 10+ console supports ANSI by default + // We could check Windows version, but being conservative here + return true; // Modern Windows usually supports colors +#else + // On POSIX systems, check the TERM environment variable + const char* term = std::getenv("TERM"); + if(!term) { + return false; // No TERM variable, probably not a color terminal + } + + std::string term_str(term); + + // Check for common indicators of color support + if(term_str.find("color") != std::string::npos || + term_str.find("xterm") != std::string::npos || + term_str.find("rxvt") != std::string::npos || + term_str.find("screen") != std::string::npos || + term_str.find("tmux") != std::string::npos || + term_str.find("linux") != std::string::npos) { + return true; + } + + // Check for dumb terminal + if(term_str == "dumb") { + return false; + } + + // For other TERM values, conservatively assume color support + // Most modern terminals support colors + return true; +#endif +} + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Check if stdout supports color output. + * + * Convenience function equivalent to `stream_supports_color(std::cout)`. + * + * \return `true` if stdout supports color output, `false` otherwise + */ +inline bool stdout_supports_color() { + return stream_supports_color(std::cout); +} + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Check if stderr supports color output. + * + * Convenience function equivalent to `stream_supports_color(std::cerr)`. + * + * \return `true` if stderr supports color output, `false` otherwise + */ +inline bool stderr_supports_color() { + return stream_supports_color(std::cerr); +} + +} // namespace IO +} // namespace CGAL + +#endif // CGAL_IO_COLOR_OSTREAM_H diff --git a/Stream_support/include/CGAL/IO/Indenting_ostream.h b/Stream_support/include/CGAL/IO/Indenting_ostream.h new file mode 100644 index 00000000000..fadc83bb2c2 --- /dev/null +++ b/Stream_support/include/CGAL/IO/Indenting_ostream.h @@ -0,0 +1,330 @@ +// Copyright (c) 2025 +// Utrecht University (The Netherlands), +// ETH Zurich (Switzerland), +// INRIA Sophia-Antipolis (France), +// Max-Planck-Institute Saarbruecken (Germany), +// and Tel-Aviv University (Israel). All rights reserved. +// +// This file is part of CGAL (www.cgal.org) +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial +// +// +// Author(s) : Laurent Rineau + +#ifndef CGAL_IO_INDENTING_OSTREAM_H +#define CGAL_IO_INDENTING_OSTREAM_H + +#include + +#include +#include +#include +#include + +namespace CGAL { +namespace IO { + +/** + * \ingroup PkgStreamSupportRef + * + * The class template `Basic_indenting_streambuf` wraps another `basic_streambuf` + * and automatically adds indentation at the beginning of each line. This is + * useful for formatting debug output with consistent indentation levels. + * + * \tparam CharT Character type (typically `char` or `wchar_t`) + * \tparam Traits Character traits type + */ +template > +class Basic_indenting_streambuf : public std::basic_streambuf, protected Traits +{ +public: + using char_type = CharT; + using traits_type = Traits; + using int_type = typename traits_type::int_type; + using pos_type = typename traits_type::pos_type; + using off_type = typename traits_type::off_type; + using streambuf_type = std::basic_streambuf; + using string = std::basic_string; + +private: + streambuf_type* wrapped_buf_; + string indent_string_; + bool at_line_start_; + +public: + /** + * \brief Construct an indenting streambuf wrapper. + * + * \param wrapped_buf The underlying streambuf to wrap + * \param indent_string The string to use for indentation (default: 2 spaces) + */ + explicit + Basic_indenting_streambuf(streambuf_type& wrapped_buf, + const string& indent_string = {2, char_type(' ')}) + : wrapped_buf_(&wrapped_buf) + , indent_string_(indent_string) + , at_line_start_(true) + { + } + + /** \brief Get the current indentation string. */ + const string& indent_string() const { return indent_string_; } + + /** + * \brief Set a new indentation string. + * + * \param new_indent The new indentation string + */ + void set_indent_string(const string& new_indent) { indent_string_ = new_indent; } + + /** + * \brief Set indentation level using repeated spaces. + * + * \param level Number of indentation levels + * \param spaces_per_level Number of spaces per level (default: 2) + */ + void set_indent_level(int level, int spaces_per_level = 2) { + indent_string_ = string(level * spaces_per_level, char_type(' ')); + } + + /** \brief Get the wrapped streambuf. */ + streambuf_type& wrapped_streambuf() const { return *wrapped_buf_; } + +protected: + using traits_type::eof; + using traits_type::not_eof; + using traits_type::to_char_type; + using traits_type::to_int_type; + using traits_type::eq_int_type; + + int_type overflow(int_type ch = eof()) override { + if(eq_int_type(ch, eof())) { + return wrapped_buf_->pubsync() == 0 ? not_eof(ch) : eof(); + } + + // If we're at the start of a line, output the indentation first + if(at_line_start_ && !indent_string_.empty()) { + for(char_type indent_char : indent_string_) { + if(eq_int_type(wrapped_buf_->sputc(indent_char), eof())) { + return eof(); + } + } + at_line_start_ = false; + } + + // Output the actual character + int_type result = wrapped_buf_->sputc(to_char_type(ch)); + + // Check if this character is a newline + if(!eq_int_type(result, eof()) && to_char_type(ch) == char_type('\n')) { + at_line_start_ = true; + } + + return result; + } + + int sync() override { return wrapped_buf_->pubsync(); } + + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + std::streamsize written = 0; + + for(std::streamsize i = 0; i < count; ++i) { + if(eq_int_type(overflow(to_int_type(s[i])), eof())) { + break; + } + ++written; + } + + return written; + } + + pos_type seekoff(off_type off, + std::ios_base::seekdir dir, + std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override { + return wrapped_buf_->pubseekoff(off, dir, which); + } + + pos_type seekpos(pos_type pos, std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override { + return wrapped_buf_->pubseekpos(pos, which); + } + + std::streamsize showmanyc() override { + return wrapped_buf_->in_avail(); + } + + std::streamsize xsgetn(char_type* s, std::streamsize count) override { + return wrapped_buf_->sgetn(s, count); + } + + int_type underflow() override { + return wrapped_buf_->sgetc(); + } + + int_type uflow() override { + return wrapped_buf_->sbumpc(); + } + + int_type pbackfail(int_type c = eof()) override { + return wrapped_buf_->sputbackc(to_char_type(c)); + } + + void imbue(const std::locale& loc) override { + wrapped_buf_->pubimbue(loc); + } + + streambuf_type* setbuf(char_type* s, std::streamsize n) override { + return wrapped_buf_->pubsetbuf(s, n); + } +}; + +/// Type alias for `Basic_indenting_streambuf` +using Indenting_streambuf = Basic_indenting_streambuf; + +/// Type alias for `Basic_indenting_streambuf` +using Indenting_wstreambuf = Basic_indenting_streambuf; + +/** + * \ingroup PkgStreamSupportRef + * + * RAII helper class for temporarily installing an indenting streambuf on a stream. + * + * This class automatically restores the original streambuf when it goes out of scope. + * It provides a convenient way to add indentation to output streams within a specific scope. + * + * \tparam StreamT The stream type (e.g., `std::ostream`, `std::wostream`) + */ +template +class Basic_indenting_stream_guard +{ +private: + using char_type = typename StreamT::char_type; + using traits_type = typename StreamT::traits_type; + using streambuf_type = Basic_indenting_streambuf; + using wrapped_streambuf_type = std::basic_streambuf; + using string = std::basic_string; + StreamT& stream_; + wrapped_streambuf_type* original_buf_; + string original_indent_string_; + streambuf_type indenting_buf_; + +public: + /** + * \brief Construct and install an indenting streambuf on the given stream. + * + * \param stream The stream to modify + * \param indent_string The indentation string to use + */ + explicit Basic_indenting_stream_guard(StreamT& stream, + const string& indent_string) + : stream_(stream) + , original_buf_(stream.rdbuf()) + , indenting_buf_(*original_buf_, indent_string) + { + if(auto old_buf = dynamic_cast(original_buf_)) { + original_indent_string_ = old_buf->indent_string(); + old_buf->set_indent_string(original_indent_string_ + indent_string); + } else { + stream.rdbuf(&indenting_buf_); + } + } + + /** + * \brief Construct and install an indenting streambuf on the given stream + * + * \param stream The stream to modify + * \param spaces_per_level Number of indentation spaces + */ + Basic_indenting_stream_guard(StreamT& stream, int spaces_per_level = 2) + : Basic_indenting_stream_guard(stream, + string(spaces_per_level, char_type(' '))) { + } + + // Non-copyable, movable + Basic_indenting_stream_guard(const Basic_indenting_stream_guard&) = delete; + Basic_indenting_stream_guard& operator=(const Basic_indenting_stream_guard&) = delete; + + Basic_indenting_stream_guard(Basic_indenting_stream_guard&& other) + : stream_(other.stream_) + , original_buf_(other.original_buf_) + , original_indent_string_(std::move(other.original_indent_string_)) + , indenting_buf_(std::move(other.indenting_buf_)) + { + // Update the stream to point to the new buffer location if it was using the old one + if(stream_.rdbuf() == &other.indenting_buf_) { + stream_.rdbuf(&indenting_buf_); + } + // Mark other as moved-from so it won't restore the stream + other.original_buf_ = nullptr; + } + + Basic_indenting_stream_guard& operator=(Basic_indenting_stream_guard&&) = delete; + + /** \brief Destructor - restores the original streambuf. */ + ~Basic_indenting_stream_guard() { + if(!original_buf_) return; // Moved-from object + if(auto old_buf = dynamic_cast(original_buf_)) { + old_buf->set_indent_string(original_indent_string_); + } else { + stream_.rdbuf(original_buf_); + } + } +}; + +/// Type alias for `basic_indenting_stream_guard` +using Indenting_stream_guard = Basic_indenting_stream_guard; + +/// Type alias for `basic_indenting_stream_guard` +using Indenting_wstream_guard = Basic_indenting_stream_guard; + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Create indenting guards for multiple streams simultaneously. + * + * This helper function creates `Basic_indenting_stream_guard` objects for + * multiple streams at once. All streams will use the same indentation settings, + * and their original streambufs will be automatically restored when the guards + * go out of scope. The guards are returned in a tuple. + * + * \tparam Streams The stream types (deduced from arguments) + * \param spaces_per_level Number of spaces per indentation level + * \param streams The streams to apply indentation to + * \return A tuple of Basic_indenting_stream_guard objects + * + * Example usage: + * \code + * auto guards = CGAL::make_indenting_guards(2, std::cout, std::cerr); + * std::cout << "Indented output\n"; + * std::cerr << "Indented error\n"; + * \endcode + */ +template +auto make_indenting_guards(int spaces_per_level, Streams&... streams) { + return std::make_tuple(Basic_indenting_stream_guard(streams, spaces_per_level)...); +} + +/** + * \ingroup PkgStreamSupportRef + * + * \brief Create indenting guards for multiple streams with a custom indent string. + * + * This overload allows specifying a custom indentation string instead of a number + * of spaces. + * + * \tparam Streams The stream types (deduced from arguments) + * \param indent_string The indentation string to use + * \param streams The streams to apply indentation to + * \return A tuple of Basic_indenting_stream_guard objects + */ +template +auto make_indenting_guards(const std::string& indent_string, Streams&... streams) { + return std::make_tuple(Basic_indenting_stream_guard(streams, indent_string)...); +} + +} // namespace IO +} // namespace CGAL + +#endif // CGAL_IO_INDENTING_OSTREAM_H \ No newline at end of file