VLink 2.0.0
A high-performance communication middleware
Loading...
Searching...
No Matches
terminal_stream.h
Go to the documentation of this file.
1/*
2 * Copyright (C) 2026 by Thun Lu. All rights reserved.
3 * Author: Thun Lu <thun.lu@zohomail.cn>
4 * Repo: https://github.com/thun-res/vlink
5 * _ __ __ _ __
6 * | | / / / / (_) ____ / /__
7 * | | / / / / / / / __ \ / //_/
8 * | |/ / / /___ / / / / / / / ,<
9 * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 */
23
24/**
25 * @file terminal_stream.h
26 * @brief Buffered, thread-safe stdout stream with TTY detection and ANSI support.
27 *
28 * @details
29 * Provides @c TerminalStream, a singleton stream with a 1-MiB internal buffer
30 * that writes to @c stdout via direct POSIX @c write() / Win32 @c _write() calls,
31 * avoiding @c std::cout and stdio overhead.
32 *
33 * The global convenience macro @c VLINK_TERM_OUT expands to @c vlink::TerminalStream::get().
34 */
35
36#pragma once
37
38#include <atomic>
39#include <cstdio>
40#include <cstring>
41#include <limits>
42#include <mutex>
43#include <string>
44#include <string_view>
45#include <type_traits>
46#include <vector>
47
48#ifdef _WIN32
49#include <Windows.h>
50#include <io.h>
51#undef min
52#undef max
53#undef GetMessage
54#define VLINK_TERM_STDOUT_FILENO _fileno(stdout)
55#define VLINK_TERM_WRITE _write
56#define VLINK_TERM_ISATTY _isatty
57#else
58#include <termios.h>
59#include <unistd.h>
60#define VLINK_TERM_STDOUT_FILENO STDOUT_FILENO
61#define VLINK_TERM_WRITE ::write
62#define VLINK_TERM_ISATTY ::isatty
63#endif
64
65namespace vlink {
66
67/**
68 * @class TerminalStream
69 * @brief Singleton buffered stdout writer with thread-safety and TTY detection.
70 *
71 * @details
72 * Obtain the singleton with @c TerminalStream::get(). Copy and move are disabled.
73 */
74class TerminalStream final {
75 public:
76 /**
77 * @brief Default internal buffer size (1 MiB).
78 */
79 static constexpr size_t kDefaultBufferSize{1024 * 1024 * 1};
80
81 /**
82 * @brief Type alias for stream manipulator functions.
83 */
85
86 /**
87 * @brief Returns the process-global @c TerminalStream singleton.
88 *
89 * @return Reference to the singleton instance.
90 */
91 static TerminalStream& get() noexcept;
92
93 /**
94 * @brief Stream manipulator that appends a newline and flushes the buffer.
95 *
96 * @details Usage: @c stream << TerminalStream::endl;
97 *
98 * @param stream The @c TerminalStream to write to.
99 * @return Reference to @p stream.
100 */
101 static TerminalStream& endl(TerminalStream& stream) noexcept;
102
103 /**
104 * @brief Stream manipulator that flushes the buffer without appending a newline.
105 *
106 * @details Usage: @c stream << TerminalStream::flush_manip;
107 *
108 * @param stream The @c TerminalStream to flush.
109 * @return Reference to @p stream.
110 */
111 static TerminalStream& flush_manip(TerminalStream& stream) noexcept;
112
113 /**
114 * @brief Constructs the stream with a @c kDefaultBufferSize internal buffer.
115 */
116 TerminalStream() noexcept;
117
118 /**
119 * @brief Destructor -- flushes any remaining buffered data to stdout.
120 */
121 ~TerminalStream() noexcept;
122
123 TerminalStream(const TerminalStream&) noexcept = delete;
124
125 TerminalStream& operator=(const TerminalStream&) noexcept = delete;
126
127 TerminalStream(TerminalStream&&) noexcept = delete;
128
129 TerminalStream& operator=(TerminalStream&&) noexcept = delete;
130
131 /**
132 * @brief Initialises the stream: detects the file descriptor and TTY status.
133 *
134 * @details
135 * Must be called once before using @c is_tty(). On Windows, enables virtual terminal
136 * processing (ANSI escape codes). Subsequent calls are no-ops.
137 */
138 void init() noexcept;
139
140 /**
141 * @brief Returns @c true if stdout is connected to a terminal (TTY).
142 *
143 * @details
144 * Only meaningful after @c init() has been called.
145 *
146 * @return @c true if stdout is a TTY.
147 */
148 [[nodiscard]] bool is_tty() const noexcept;
149
150 /**
151 * @brief Returns @c true if @c init() has been called.
152 *
153 * @return @c true after the first successful @c init() call.
154 */
155 [[nodiscard]] bool is_initialized() const noexcept;
156
157 /**
158 * @brief Flushes all buffered data to stdout immediately.
159 *
160 * @details
161 * On non-Windows TTY outputs, also calls @c tcdrain() to wait for the terminal.
162 *
163 * @note This method acquires the internal mutex; safe to call concurrently.
164 */
165 void flush();
166
167 /**
168 * @brief Writes a raw byte array of @p len bytes to the buffer.
169 *
170 * @param data Pointer to data to write.
171 * @param len Number of bytes to write.
172 * @return Reference to @c *this for chaining.
173 */
174 TerminalStream& write_raw(const char* data, size_t len) noexcept;
175
176 /**
177 * @brief Writes a single character to the buffer.
178 *
179 * @param c Character to write.
180 * @return Reference to @c *this.
181 */
182 TerminalStream& operator<<(char c) noexcept;
183
184 /**
185 * @brief Writes a NUL-terminated C string to the buffer.
186 *
187 * @details Null-pointer check is performed; passing @c nullptr is a no-op.
188 *
189 * @param str C string to write.
190 * @return Reference to @c *this.
191 */
192 TerminalStream& operator<<(const char* str) noexcept;
193
194 /**
195 * @brief Writes a @c std::string to the buffer.
196 *
197 * @param str String to write.
198 * @return Reference to @c *this.
199 */
200 TerminalStream& operator<<(const std::string& str) noexcept;
201
202 /**
203 * @brief Writes a @c std::string_view to the buffer.
204 *
205 * @param str String view to write.
206 * @return Reference to @c *this.
207 */
208 TerminalStream& operator<<(std::string_view str) noexcept;
209
210 // NOLINTBEGIN
211
212 /**
213 * @brief Writes @c "true" or @c "false".
214 *
215 * @return Reference to @c *this.
216 */
217 TerminalStream& operator<<(bool value) noexcept;
218
219 /**
220 * @brief Writes a short integer.
221 *
222 * @return Reference to @c *this.
223 */
224 TerminalStream& operator<<(short value) noexcept;
225
226 /**
227 * @brief Writes an unsigned short integer.
228 *
229 * @return Reference to @c *this.
230 */
231 TerminalStream& operator<<(unsigned short value) noexcept;
232
233 /**
234 * @brief Writes an int.
235 *
236 * @return Reference to @c *this.
237 */
238 TerminalStream& operator<<(int value) noexcept;
239
240 /**
241 * @brief Writes an unsigned int.
242 *
243 * @return Reference to @c *this.
244 */
245 TerminalStream& operator<<(unsigned int value) noexcept;
246
247 /**
248 * @brief Writes a long.
249 *
250 * @return Reference to @c *this.
251 */
252 TerminalStream& operator<<(long value) noexcept;
253
254 /**
255 * @brief Writes an unsigned long.
256 *
257 * @return Reference to @c *this.
258 */
259 TerminalStream& operator<<(unsigned long value) noexcept;
260
261 /**
262 * @brief Writes a long long.
263 *
264 * @return Reference to @c *this.
265 */
266 TerminalStream& operator<<(long long value) noexcept;
267
268 /**
269 * @brief Writes an unsigned long long.
270 *
271 * @return Reference to @c *this.
272 */
273 TerminalStream& operator<<(unsigned long long value) noexcept;
274
275 /**
276 * @brief Writes a float using @c "%g" format.
277 *
278 * @return Reference to @c *this.
279 */
280 TerminalStream& operator<<(float value) noexcept;
281
282 /**
283 * @brief Writes a double using @c "%g" format.
284 *
285 * @return Reference to @c *this.
286 */
287 TerminalStream& operator<<(double value) noexcept;
288
289 /**
290 * @brief Writes a long double using @c "%Lg" format.
291 *
292 * @return Reference to @c *this.
293 */
294 TerminalStream& operator<<(long double value) noexcept;
295
296 /**
297 * @brief Writes a pointer address as a hex string.
298 *
299 * @return Reference to @c *this.
300 */
301 TerminalStream& operator<<(const void* ptr) noexcept;
302
303 // NOLINTEND
304
305 /**
306 * @brief Applies a @c ManipType manipulator function to the stream.
307 *
308 * @details
309 * Compatible with @c TerminalStream::endl and @c TerminalStream::flush_manip.
310 *
311 * @param manip Manipulator function pointer.
312 * @return Reference to @c *this.
313 */
314 TerminalStream& operator<<(ManipType manip) noexcept;
315
316 /**
317 * @brief Accepts @c std::endl and similar @c std::ostream manipulators.
318 *
319 * @details
320 * When @c std::endl is passed, writes a newline and flushes the buffer.
321 *
322 * @return Reference to @c *this.
323 */
324 TerminalStream& operator<<(std::ostream& (*)(std::ostream&)) noexcept;
325
326 private:
327 void flush_unlocked() noexcept;
328
329 void write_to_buffer(const char* data, size_t len) noexcept;
330
331 template <typename T>
332 TerminalStream& write_integer(T value) noexcept;
333
334 template <typename T>
335 TerminalStream& write_float(T value) noexcept;
336
337 std::atomic_bool initialized_{false};
338 std::atomic_bool is_tty_{false};
339
340 mutable std::mutex mutex_;
341 std::vector<char> buffer_;
342 size_t write_pos_{0};
344};
345
346////////////////////////////////////////////////////////////////
347/// Implementation Details
348////////////////////////////////////////////////////////////////
349
351 static TerminalStream instance;
352 return instance;
353}
354
356 std::lock_guard lock(stream.mutex_);
357 stream.write_to_buffer("\n", 1);
358 stream.flush_unlocked();
359 return stream;
360}
361
363 std::lock_guard lock(stream.mutex_);
364 stream.flush_unlocked();
365 return stream;
366}
367
369
371 std::lock_guard lock(mutex_);
372 flush_unlocked();
373}
374
375inline void TerminalStream::init() noexcept {
376 bool expected = false;
377
378 if (!initialized_.compare_exchange_strong(expected, true)) {
379 return;
380 }
381
382 std::lock_guard lock(mutex_);
383
385
386#ifdef _WIN32
387 HANDLE h_out = GetStdHandle(STD_OUTPUT_HANDLE);
388
389 if (h_out != INVALID_HANDLE_VALUE) {
390 DWORD dw_mode = 0;
391
392 if (GetConsoleMode(h_out, &dw_mode)) {
393 dw_mode |= 0x0004; // ENABLE_VIRTUAL_TERMINAL_PROCESSING
394 SetConsoleMode(h_out, dw_mode);
395 }
396 }
397#endif
398
399 is_tty_.store(VLINK_TERM_ISATTY(fd_) != 0, std::memory_order_release);
400}
401
402inline bool TerminalStream::is_tty() const noexcept { return is_tty_.load(std::memory_order_acquire); }
403
404inline bool TerminalStream::is_initialized() const noexcept { return initialized_.load(std::memory_order_acquire); }
405
407 std::lock_guard lock(mutex_);
408 flush_unlocked();
409}
410
411inline TerminalStream& TerminalStream::write_raw(const char* data, size_t len) noexcept {
412 std::lock_guard lock(mutex_);
413 write_to_buffer(data, len);
414 return *this;
415}
416
418 std::lock_guard lock(mutex_);
419 write_to_buffer(&c, 1);
420 return *this;
421}
422
423inline TerminalStream& TerminalStream::operator<<(const char* str) noexcept {
424 if (str == nullptr) {
425 return *this;
426 }
427
428 std::lock_guard lock(mutex_);
429 write_to_buffer(str, std::strlen(str));
430 return *this;
431}
432
433inline TerminalStream& TerminalStream::operator<<(const std::string& str) noexcept {
434 std::lock_guard lock(mutex_);
435 write_to_buffer(str.data(), str.size());
436 return *this;
437}
438
439inline TerminalStream& TerminalStream::operator<<(std::string_view str) noexcept {
440 std::lock_guard lock(mutex_);
441 write_to_buffer(str.data(), str.size());
442 return *this;
443}
444
445// NOLINTBEGIN
446
447inline TerminalStream& TerminalStream::operator<<(bool value) noexcept { return *this << (value ? "true" : "false"); }
448
449inline TerminalStream& TerminalStream::operator<<(short value) noexcept { return write_integer(value); }
450
451inline TerminalStream& TerminalStream::operator<<(unsigned short value) noexcept { return write_integer(value); }
452
453inline TerminalStream& TerminalStream::operator<<(int value) noexcept { return write_integer(value); }
454
455inline TerminalStream& TerminalStream::operator<<(unsigned int value) noexcept { return write_integer(value); }
456
457inline TerminalStream& TerminalStream::operator<<(long value) noexcept { return write_integer(value); }
458
459inline TerminalStream& TerminalStream::operator<<(unsigned long value) noexcept { return write_integer(value); }
460
461inline TerminalStream& TerminalStream::operator<<(long long value) noexcept { return write_integer(value); }
462
463inline TerminalStream& TerminalStream::operator<<(unsigned long long value) noexcept { return write_integer(value); }
464
465inline TerminalStream& TerminalStream::operator<<(float value) noexcept { return write_float(value); }
466
467inline TerminalStream& TerminalStream::operator<<(double value) noexcept { return write_float(value); }
468
469inline TerminalStream& TerminalStream::operator<<(long double value) noexcept { return write_float(value); }
470
471inline TerminalStream& TerminalStream::operator<<(const void* ptr) noexcept {
472 std::lock_guard lock(mutex_);
473
474 char buf[32];
475
476 int len = std::snprintf(buf, sizeof(buf), "%p", ptr);
477
478 if (len > 0) {
479 write_to_buffer(buf, static_cast<size_t>(len));
480 }
481
482 return *this;
483}
484
485// NOLINTEND
486
487inline TerminalStream& TerminalStream::operator<<(ManipType manip) noexcept { return manip(*this); }
488
489inline TerminalStream& TerminalStream::operator<<(std::ostream& (*)(std::ostream&)) noexcept {
490 std::lock_guard lock(mutex_);
491
492 write_to_buffer("\n", 1);
493 flush_unlocked();
494
495 return *this;
496}
497
498inline void TerminalStream::flush_unlocked() noexcept {
499 if (write_pos_ == 0) {
500 return;
501 }
502
503 const char* data = buffer_.data();
504 size_t remaining = write_pos_;
505
506 while (remaining > 0) {
507 auto written = VLINK_TERM_WRITE(fd_, data, static_cast<unsigned int>(remaining));
508
509 if (written <= 0) {
510 break;
511 }
512
513 data += written;
514 remaining -= static_cast<size_t>(written);
515 }
516
517 write_pos_ = 0;
518
519#ifndef _WIN32
520 if (is_tty_.load(std::memory_order_acquire)) {
521 ::tcdrain(fd_);
522 }
523#endif
524}
525
526inline void TerminalStream::write_to_buffer(const char* data, size_t len) noexcept {
527 if (len == 0) {
528 return;
529 }
530
531 if (len >= buffer_.size()) {
532 flush_unlocked();
533
534 size_t remaining = len;
535 while (remaining > 0) {
536 auto written = VLINK_TERM_WRITE(fd_, data, static_cast<unsigned int>(remaining));
537
538 if (written <= 0) {
539 break;
540 }
541
542 data += written;
543 remaining -= static_cast<size_t>(written);
544 }
545
546 return;
547 }
548
549 if (write_pos_ + len > buffer_.size()) {
550 flush_unlocked();
551 }
552
553 std::memcpy(buffer_.data() + write_pos_, data, len);
554 write_pos_ += len;
555}
556
557template <typename T>
558inline TerminalStream& TerminalStream::write_integer(T value) noexcept {
559 std::lock_guard lock(mutex_);
560
561 char buf[32];
562 char* ptr = buf + sizeof(buf);
563 bool negative = false;
564
565 if constexpr (std::is_signed_v<T>) {
566 if (value < 0) {
567 negative = true;
568
569 if (value == std::numeric_limits<T>::min()) {
570 int len = std::snprintf(buf, sizeof(buf), "%lld",
571 static_cast<long long>(value)); // NOLINT(runtime/int, google-runtime-int)
572
573 if (len > 0) {
574 write_to_buffer(buf, static_cast<size_t>(len));
575 }
576
577 return *this;
578 }
579
580 value = -value;
581 }
582 }
583
584 if (value == 0) {
585 *--ptr = '0';
586 } else {
587 while (value != 0) {
588 *--ptr = '0' + static_cast<char>(value % 10);
589 value /= 10;
590 }
591 }
592
593 if (negative) {
594 *--ptr = '-';
595 }
596
597 write_to_buffer(ptr, static_cast<size_t>(buf + sizeof(buf) - ptr));
598
599 return *this;
600}
601
602template <typename T>
603inline TerminalStream& TerminalStream::write_float(T value) noexcept {
604 std::lock_guard lock(mutex_);
605
606 char buf[64];
607 int len;
608
609 // NOLINTNEXTLINE(google-runtime-float)
610 if constexpr (std::is_same_v<T, long double>) {
611 len = std::snprintf(buf, sizeof(buf), "%Lg", value);
612 } else {
613 len = std::snprintf(buf, sizeof(buf), "%g", static_cast<double>(value));
614 }
615
616 if (len > 0) {
617 write_to_buffer(buf, static_cast<size_t>(len));
618 }
619
620 return *this;
621}
622
623} // namespace vlink
624
625#ifndef VLINK_TERM_OUT
626#define VLINK_TERM_OUT vlink::TerminalStream::get()
627#endif
STL namespace.
#define VLINK_TERM_WRITE
Definition terminal_stream.h:61
#define VLINK_TERM_STDOUT_FILENO
Definition terminal_stream.h:60
#define VLINK_TERM_ISATTY
Definition terminal_stream.h:62