Skip to content

Commit dfe8ea6

Browse files
committed
Refactor signal handling for robustness and platform correctness
- POSIX: use a signal-safe self-pipe mechanism and safer shutdown handling - Windows: switch to console control handler and improve lifecycle handling Overall improves robustness, correctness, and alignment with platform expectations.
1 parent a8ece19 commit dfe8ea6

17 files changed

Lines changed: 588 additions & 129 deletions

‎doc/chapters/reference/terminal.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,8 @@ Interface
235235
:members:
236236

237237
.. doxygentypedef:: erbsland::cterm::TerminalPtr
238+
239+
.. doxygenenum:: erbsland::cterm::TerminalFlag
240+
241+
.. doxygenclass:: erbsland::cterm::TerminalFlags
242+
:members:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#pragma once
2+
#include "../../../src/erbsland/cterm/TerminalFlags.hpp"
3+
// !!! WARNING - THIS IS A GENERATED FILE - DO NOT EDIT MANUALLY !!!
4+
// This file was generated by the `generate_header_files.py` script.
5+

‎src/erbsland/cterm/Backend.hpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "Input.hpp"
88
#include "MoveMode.hpp"
99
#include "Size.hpp"
10+
#include "TerminalFlags.hpp"
1011

1112
#include <memory>
1213
#include <optional>
@@ -119,15 +120,15 @@ class Backend : public std::enable_shared_from_this<Backend> {
119120
/// In `Input::Mode::ReadLine` mode, the user has to enter a character and press enter.
120121
/// @param timeout Maximum wait time in `Mode::Key`; ignored in `Mode::ReadLine`.
121122
/// @return The parsed key event, or an invalid key if no supported input was read.
122-
[[nodiscard]] virtual auto readKey(std::chrono::milliseconds timeout = {}) -> Key = 0;
123+
[[nodiscard]] virtual auto readKey(std::chrono::milliseconds timeout) -> Key = 0;
123124
/// Read text input in the terminal.
124125
/// Can be ignored in `Input::Mode::Key` mode.
125126
/// @return The read text, without line breaks.
126127
[[nodiscard]] virtual auto readLine() -> std::string = 0;
127128

128129
public:
129130
/// Create the default backend for this platform.
130-
[[nodiscard]] static auto createPlatformDefault() -> BackendPtr;
131+
[[nodiscard]] static auto createPlatformDefault(TerminalFlags terminalFlags) -> BackendPtr;
131132
};
132133

133134

‎src/erbsland/cterm/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ target_sources(erbsland-color-term PRIVATE
5353
String.hpp
5454
Terminal.cpp
5555
Terminal.hpp
56+
TerminalFlags.hpp
5657
Text.hpp
5758
TextAnimation.hpp
5859
Tile9Style.cpp

‎src/erbsland/cterm/Terminal.cpp‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,24 @@
1414
namespace erbsland::cterm {
1515

1616

17-
Terminal::Terminal(const Size size) : _size{size.componentMin(cMaximumSize).componentMax(cMinimumSize)} {
18-
_backend = Backend::createPlatformDefault();
17+
Terminal::Terminal() : Terminal(Size{80, 25}, TerminalFlags{}) {
18+
}
19+
20+
Terminal::Terminal(const TerminalFlags flags) : Terminal(Size{80, 25}, flags) {
21+
}
22+
23+
Terminal::Terminal(const Size size, const TerminalFlags flags) :
24+
_flags{flags}, _size{size.componentMin(cMaximumSize).componentMax(cMinimumSize)} {
25+
_backend = Backend::createPlatformDefault(flags);
26+
_input.setBackend(_backend);
27+
_lineBuffer.setBackend(_backend);
28+
}
29+
30+
Terminal::Terminal(BackendPtr backend, const Size size) :
31+
_backend{std::move(backend)}, _size{size.componentMin(cMaximumSize).componentMax(cMinimumSize)} {
32+
if (_backend == nullptr) {
33+
_backend = Backend::createPlatformDefault(TerminalFlags{});
34+
}
1935
_input.setBackend(_backend);
2036
_lineBuffer.setBackend(_backend);
2137
}
@@ -96,7 +112,7 @@ void Terminal::setBackBufferEnabled(const bool enabled) noexcept {
96112
void Terminal::setBackend(BackendPtr backend) noexcept {
97113
if (_backend != backend) {
98114
if (backend == nullptr) {
99-
_backend = Backend::createPlatformDefault();
115+
_backend = Backend::createPlatformDefault(_flags);
100116
} else {
101117
_backend = std::move(backend);
102118
}

‎src/erbsland/cterm/Terminal.hpp‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "MoveMode.hpp"
1010
#include "Size.hpp"
1111
#include "String.hpp"
12+
#include "TerminalFlags.hpp"
1213
#include "UpdateSettings.hpp"
1314
#include "WritableBuffer.hpp"
1415

@@ -64,10 +65,21 @@ class Terminal {
6465
};
6566

6667
public:
68+
/// Create a new terminal instance with default values.
69+
explicit Terminal();
6770
/// Create a new terminal instance.
71+
/// @param flags The terminal flags to use.
72+
explicit Terminal(TerminalFlags flags = {});
73+
/// Create a new terminal instance.
74+
/// The size is automatically bounded to the minimum and maximum supported sizes.
75+
/// @param size The fallback terminal size used when automatic detection is unavailable.
76+
/// @param flags The terminal flags to use.
77+
explicit Terminal(Size size = {80, 25}, TerminalFlags flags = {});
78+
/// Create a new terminal instance with a custom backend.
6879
/// The size is automatically bounded to the minimum and maximum supported sizes.
80+
/// @param backend The backend to use for the terminal.
6981
/// @param size The fallback terminal size used when automatic detection is unavailable.
70-
explicit Terminal(Size size = {80, 25});
82+
explicit Terminal(BackendPtr backend, Size size = {80, 25});
7183

7284
public: // settings
7385
/// Get the currently configured or detected drawable terminal size.
@@ -93,7 +105,7 @@ class Terminal {
93105
/// Set if dynamic terminal size detection is enabled.
94106
/// Can only be enabled while the output mode is `OutputMode::FullControl`.
95107
/// @param enabled `true` to enable automatic size detection.
96-
void setSizeDetectionEnabled(const bool enabled) noexcept;
108+
void setSizeDetectionEnabled(bool enabled) noexcept;
97109
/// Check whether line buffering is enabled for incremental writes.
98110
/// @return `true` if text output is collected until a newline or `flush()`.
99111
[[nodiscard]] auto lineBufferEnabled() const noexcept -> bool;
@@ -294,6 +306,7 @@ class Terminal {
294306
void printLinePart(const char text[]) noexcept { write(text); }
295307

296308
private:
309+
TerminalFlags _flags; ///< Flags for the terminal behaviour.
297310
BackendPtr _backend; ///< The backend that is used by the terminal.
298311
OutputMode _outputMode{OutputMode::FullControl}; ///< The current output mode.
299312
bool _sizeDetectionEnabled{true}; ///< If size detection is enabled.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
2+
// SPDX-License-Identifier: Apache-2.0
3+
#pragma once
4+
5+
6+
#include <cstdint>
7+
#include <type_traits>
8+
9+
10+
namespace erbsland::cterm {
11+
12+
/// A terminal flag.
13+
enum class TerminalFlag : uint8_t {
14+
/// Disables signal handling to restore the screen when the application is terminated.
15+
/// If this flag is set, you must ensure that the `restoreScreen()` method is called when the application
16+
/// is terminated by a signal. Otherwise, the terminal will not be restored properly.
17+
NoSignalHandling = 1 << 0,
18+
};
19+
20+
/// A set of terminal flags.
21+
/// Terminal flags control the behavior of the built-in terminal backend.
22+
/// These flags can only be set at construction time and cannot be modified after that.
23+
class TerminalFlags {
24+
public:
25+
using Mask = uint8_t;
26+
using Enum = TerminalFlag;
27+
28+
public:
29+
/// Create a combined set of flags.
30+
template <typename... tFlags>
31+
requires(sizeof...(tFlags) > 0 && (std::is_same_v<Enum, tFlags> && ...))
32+
constexpr TerminalFlags(tFlags... flags) : // NOLINT(*-explicit-constructor)
33+
_flags{static_cast<Mask>((static_cast<Mask>(flags) | ...))} {}
34+
35+
// defaults
36+
TerminalFlags() = default;
37+
TerminalFlags(const TerminalFlags &) = default;
38+
auto operator=(const TerminalFlags &) -> TerminalFlags & = default;
39+
40+
public:
41+
auto operator|(const TerminalFlag flag) const -> TerminalFlags {
42+
return TerminalFlags{static_cast<Mask>(_flags | static_cast<Mask>(flag))};
43+
}
44+
friend auto operator|(const TerminalFlag flag, const TerminalFlags flags) -> TerminalFlags {
45+
return TerminalFlags{static_cast<Mask>(flags._flags | static_cast<Mask>(flag))};
46+
}
47+
friend auto operator|(const TerminalFlag flag1, const TerminalFlag flag2) -> TerminalFlags {
48+
return TerminalFlags{flag1, flag2};
49+
}
50+
friend auto operator|(const TerminalFlags flags1, const TerminalFlags flags2) -> TerminalFlags {
51+
return TerminalFlags{static_cast<Mask>(flags1._flags | flags2._flags)};
52+
}
53+
54+
public:
55+
/// Test if a flag is set.
56+
[[nodiscard]] auto has(const TerminalFlag flag) const noexcept -> bool {
57+
return (_flags & static_cast<Mask>(flag)) != 0;
58+
}
59+
/// Set a flag.
60+
void set(const TerminalFlag flag, const bool enabled = true) {
61+
if (enabled) {
62+
_flags |= static_cast<Mask>(flag);
63+
} else {
64+
_flags &= ~static_cast<Mask>(flag);
65+
}
66+
}
67+
/// Clear a flag.
68+
void clear(const TerminalFlag flag) { _flags &= ~static_cast<Mask>(flag); }
69+
70+
private:
71+
explicit TerminalFlags(const Mask flags) : _flags(flags) {}
72+
73+
private:
74+
Mask _flags{0};
75+
};
76+
77+
}

‎src/erbsland/cterm/all.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "Size.hpp"
3838
#include "String.hpp"
3939
#include "Terminal.hpp"
40+
#include "TerminalFlags.hpp"
4041
#include "Text.hpp"
4142
#include "TextAnimation.hpp"
4243
#include "Tile9Style.hpp"

‎src/erbsland/cterm/impl/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ if (WIN32)
2626
target_sources(erbsland-color-term PRIVATE
2727
WindowsBackend.cpp
2828
WindowsBackend.hpp
29+
WindowsSignalDispatcher.cpp
30+
WindowsSignalDispatcher.hpp
2931
)
3032
else ()
3133
target_sources(erbsland-color-term PRIVATE
3234
PosixBackend.cpp
3335
PosixBackend.hpp
36+
PosixSignalDispatcher.cpp
37+
PosixSignalDispatcher.hpp
3438
)
3539
endif ()

‎src/erbsland/cterm/impl/PosixBackend.cpp‎

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22
// SPDX-License-Identifier: Apache-2.0
33
#include "PosixBackend.hpp"
44

5+
6+
#include "PosixSignalDispatcher.hpp"
7+
58
#include <fcntl.h>
69
#include <sys/ioctl.h>
710
#include <sys/select.h>
811
#include <unistd.h>
912

10-
#include <array>
11-
#include <csignal>
1213
#include <iostream>
1314
#include <thread>
1415

1516

1617
namespace erbsland::cterm {
1718

18-
auto Backend::createPlatformDefault() -> BackendPtr {
19-
return impl::PosixBackend::getOrCreate();
19+
auto Backend::createPlatformDefault(const TerminalFlags terminalFlags) -> BackendPtr {
20+
return impl::PosixBackend::getOrCreate(terminalFlags);
2021
}
2122

2223
}
@@ -25,17 +26,19 @@ namespace erbsland::cterm::impl {
2526

2627
std::mutex PosixBackend::_instanceMutex;
2728
PosixBackend *PosixBackend::_instance = nullptr;
28-
std::array<struct sigaction, PosixBackend::cSignals.size()> PosixBackend::_previousSignalActions{};
2929

30-
PosixBackend::PosixBackend() {
30+
PosixBackend::PosixBackend(TerminalFlags terminalFlags) : _terminalFlags{terminalFlags} {
3131
// called once per application.
3232
_instance = this;
33-
registerExitHandlers();
33+
if (!_terminalFlags.has(TerminalFlag::NoSignalHandling)) {
34+
_signalHandler = std::make_unique<PosixSignalDispatcher>(
35+
[this](const int signalNumber) -> void { handleProcessSignal(signalNumber); });
36+
}
3437
}
3538

3639
PosixBackend::~PosixBackend() {
37-
unregisterExitHandlers();
38-
std::lock_guard lock{_instanceMutex};
40+
_signalHandler.reset();
41+
std::scoped_lock lock{_instanceMutex};
3942
if (_instance != nullptr) {
4043
_instance->restorePlatform();
4144
_instance = nullptr;
@@ -57,9 +60,9 @@ void PosixBackend::restorePlatform() {
5760
if (_isAlternateScreenActive) {
5861
std::cout << "\x1b[?1049l"; // disable alternative screen buffer
5962
}
60-
std::cout << "\x1b[0m"; // restore to default color
61-
std::cout << "\x1b[?25h"; // make the cursor visible.
62-
std::cout << "\n"; // add a newline for compatibility.
63+
std::cout << "\x1b[0m"; // restore to default color
64+
std::cout << "\x1b[?25h"; // make the cursor visible.
65+
std::cout << "\n"; // add a newline for compatibility.
6366
std::cout.flush();
6467
}
6568
if (_keyInputSessionActive) {
@@ -191,21 +194,21 @@ auto PosixBackend::readLine() -> std::string {
191194
return input;
192195
}
193196

194-
auto PosixBackend::getOrCreate() noexcept -> BackendPtr {
195-
std::lock_guard lock{_instanceMutex};
197+
auto PosixBackend::getOrCreate(const TerminalFlags terminalFlags) noexcept -> BackendPtr {
198+
std::scoped_lock lock{_instanceMutex};
196199
if (_instance == nullptr) {
197-
return std::make_shared<PosixBackend>();
200+
return std::make_shared<PosixBackend>(terminalFlags);
198201
}
199202
return _instance->shared_from_this();
200203
}
201204

202205
auto PosixBackend::instance() noexcept -> PosixBackend * {
203-
std::lock_guard lock(_instanceMutex);
206+
std::scoped_lock lock(_instanceMutex);
204207
return _instance;
205208
}
206209

207210
void PosixBackend::restoreGlobalPlatform() noexcept {
208-
std::lock_guard lock(_instanceMutex);
211+
std::scoped_lock lock(_instanceMutex);
209212
if (_instance == nullptr) {
210213
return;
211214
}
@@ -300,31 +303,14 @@ void PosixBackend::restoreKeyInputSession() {
300303
_keyInputSessionActive = false;
301304
}
302305

303-
void PosixBackend::registerExitHandlers() {
304-
std::atexit(&PosixBackend::onExit);
306+
void PosixBackend::handleProcessSignal(const int signalNumber) noexcept {
307+
restoreGlobalPlatform();
305308
struct sigaction action{};
306-
action.sa_handler = &PosixBackend::onSignal;
309+
action.sa_handler = SIG_DFL;
307310
sigemptyset(&action.sa_mask);
308311
action.sa_flags = 0;
309-
for (std::size_t index = 0; index < cSignals.size(); ++index) {
310-
sigaction(cSignals[index], &action, &_previousSignalActions[index]);
311-
}
312-
}
313-
314-
void PosixBackend::unregisterExitHandlers() {
315-
for (std::size_t index = 0; index < cSignals.size(); ++index) {
316-
sigaction(cSignals[index], &_previousSignalActions[index], nullptr);
317-
}
318-
}
319-
320-
void PosixBackend::onExit() noexcept {
321-
restoreGlobalPlatform();
322-
}
323-
324-
void PosixBackend::onSignal(const int signalNumber) noexcept {
325-
restoreGlobalPlatform();
326-
std::signal(signalNumber, SIG_DFL);
327-
std::raise(signalNumber);
312+
sigaction(signalNumber, &action, nullptr);
313+
kill(getpid(), signalNumber);
328314
std::_Exit(128 + signalNumber);
329315
}
330316

0 commit comments

Comments
 (0)