Skip to content

Commit cf81680

Browse files
Fix/truecolor windows 1305 (#1307)
* fix: restore TrueColor support on Windows Terminal (fix #1305) * Fix terminal color support issues and add automated Windows VM testing script - Honor the NO_COLOR environment variable. - Fix color support detection on Apple's Terminal.app. - Correctly treat empty terminal name / terminal emulator name as unidentified. - Safely downgrade to Palette16 on legacy Windows consoles rejecting VT processing. - Add tools/windows-vm.sh and tools/windows-vm/provision.ps1 to automate VM spin-up, provisioning, and testing.
1 parent ae84933 commit cf81680

8 files changed

Lines changed: 511 additions & 9 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ out/
8888
!tools/**/*.sh
8989
!tools/**/*.py
9090
!tools/**/*.cpp
91+
!tools/**/*.ps1
9192
!tools/abi_fingerprint.txt
9293
!tools/abidiff_suppressions.ini
94+
# Windows VM disk images and ssh keys, created by tools/windows-vm.sh:
95+
tools/windows-vm/data/
9396
build/
9497
build_asan/
9598
builddir/

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ Changelog
44
Next
55
====
66

7+
### Screen
8+
- Bugfix: Restore TrueColor support on Windows Terminal (default to TrueColor on Windows and check `WT_SESSION` environment variable for WSL compatibility). See #1305.
9+
- Feature: Honor the `NO_COLOR` environment variable (https://no-color.org). When set and non-empty, colors degrade to the terminal's default colors.
10+
- Bugfix: Apple's Terminal.app (`TERM_PROGRAM=Apple_Terminal`) is now reported as `Palette256` instead of `TrueColor`; it does not support 24bit colors.
11+
- Bugfix: An empty terminal name or terminal emulator name is now treated as unidentified by `Terminal::ComputeColorSupport`, instead of implying TrueColor support.
12+
- Bugfix (Windows): Downgrade color support when the console rejects VT processing (legacy consoles), instead of emitting TrueColor escape sequences.
13+
714
### Build
815
- Bugfix: Fix build failure when an older FTXUI is installed in a system
916
include path (e.g. MacPorts upgrade). A CMake deduplication quirk was
1017
promoting the project's own `-I include/` to `-isystem`, causing package
1118
managers' `-I/opt/local/include` (which may contain stale headers) to
1219
win. See #1299, #1300.
1320

21+
1422
7.0.0 (2026-06-13)
1523
------------------
1624

src/ftxui/screen/color.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,22 @@ Color::Color(Palette1 /*value*/) : Color() {}
101101

102102
/// @brief Build a color using the Palette16 colors.
103103
Color::Color(Palette16 index)
104-
: type_(ColorType::Palette16), red_(index), alpha_(255) {}
104+
: type_(ColorType::Palette16), red_(index), alpha_(255) {
105+
if (Terminal::ColorSupport() == Terminal::Color::Palette1) {
106+
type_ = ColorType::Palette1;
107+
}
108+
}
105109

106110
/// @brief Build a color using Palette256 colors.
107111
Color::Color(Palette256 index)
108112
: type_(ColorType::Palette256), red_(index), alpha_(255) {
109113
if (Terminal::ColorSupport() >= Terminal::Color::Palette256) {
110114
return;
111115
}
116+
if (Terminal::ColorSupport() == Terminal::Color::Palette1) {
117+
type_ = ColorType::Palette1;
118+
return;
119+
}
112120
type_ = ColorType::Palette16;
113121
red_ = GetColorInfo(Color::Palette256(red_)).index_16;
114122
}
@@ -129,6 +137,10 @@ Color::Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha)
129137
if (Terminal::ColorSupport() == Terminal::Color::TrueColor) {
130138
return;
131139
}
140+
if (Terminal::ColorSupport() == Terminal::Color::Palette1) {
141+
type_ = ColorType::Palette1;
142+
return;
143+
}
132144

133145
// Find the closest Color from the database:
134146
const int max_distance = 256 * 256 * 3;

src/ftxui/screen/color_test.cpp

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,45 @@
33
// the LICENSE file.
44
#include "ftxui/screen/color.hpp"
55
#include <gtest/gtest.h>
6+
#include <cstdlib> // for std::getenv, setenv, unsetenv
7+
#include <optional>
8+
#include <string>
69
#include "ftxui/screen/terminal.hpp"
710

811
namespace ftxui {
912

13+
#if !defined(_WIN32)
14+
namespace {
15+
// Clear an environment variable for the duration of a scope, restoring its
16+
// original value on destruction. ComputeColorSupport reads NO_COLOR and
17+
// WT_SESSION from the environment; tests must not depend on the machine
18+
// running them.
19+
class ScopedClearEnv {
20+
public:
21+
explicit ScopedClearEnv(const char* name) : name_(name) {
22+
const char* value = std::getenv(name); // NOLINT
23+
if (value != nullptr) {
24+
saved_ = value;
25+
}
26+
unsetenv(name);
27+
}
28+
~ScopedClearEnv() {
29+
if (saved_) {
30+
setenv(name_, saved_->c_str(), 1);
31+
} else {
32+
unsetenv(name_);
33+
}
34+
}
35+
ScopedClearEnv(const ScopedClearEnv&) = delete;
36+
ScopedClearEnv& operator=(const ScopedClearEnv&) = delete;
37+
38+
private:
39+
const char* name_;
40+
std::optional<std::string> saved_;
41+
};
42+
} // namespace
43+
#endif
44+
1045
TEST(ColorTest, PrintTransparent) {
1146
Terminal::SetColorSupport(Terminal::Color::TrueColor);
1247
EXPECT_EQ(Color().Print(false), "39");
@@ -83,4 +118,86 @@ TEST(ColorTest, HSV) {
83118
EXPECT_EQ(Color::HSV(0, 255, 255).Print(false), "38;2;255;0;0");
84119
}
85120

121+
TEST(ColorTest, ComputeColorSupport) {
122+
#if !defined(_WIN32)
123+
const ScopedClearEnv no_color("NO_COLOR");
124+
const ScopedClearEnv wt_session("WT_SESSION");
125+
126+
EXPECT_EQ(Terminal::ComputeColorSupport("xterm", "256", "", "", "", {}),
127+
Terminal::Color::Palette256);
128+
EXPECT_EQ(Terminal::ComputeColorSupport("xterm-256color", "", "", "", "", {}),
129+
Terminal::Color::Palette256);
130+
131+
// An unidentified terminal without any hint only gets Palette16.
132+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "", "unknown", "unknown", {}),
133+
Terminal::Color::Palette16);
134+
// An empty terminal name is treated as unidentified.
135+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "", "", "", {}),
136+
Terminal::Color::Palette16);
137+
138+
// Apple's Terminal.app supports 256 colors, but not 24bit ones.
139+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "Apple_Terminal", "unknown",
140+
"unknown", {}),
141+
Terminal::Color::Palette256);
142+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "iTerm.app", "unknown",
143+
"unknown", {}),
144+
Terminal::Color::TrueColor);
145+
#endif
146+
EXPECT_EQ(Terminal::ComputeColorSupport("xterm", "truecolor", "", "", "", {}),
147+
Terminal::Color::TrueColor);
148+
EXPECT_EQ(Terminal::ComputeColorSupport("kitty", "", "", "", "", {}),
149+
Terminal::Color::TrueColor);
150+
}
151+
152+
#if !defined(_WIN32)
153+
TEST(ColorTest, ComputeColorSupportWTSession) {
154+
const ScopedClearEnv no_color("NO_COLOR");
155+
const ScopedClearEnv wt_session("WT_SESSION");
156+
157+
// Without WT_SESSION, an unidentified terminal only gets Palette16.
158+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "", "unknown", "unknown", {}),
159+
Terminal::Color::Palette16);
160+
161+
// Windows Terminal sets WT_SESSION, including when running under WSL.
162+
setenv("WT_SESSION", "test_session_id", 1);
163+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "", "unknown", "unknown", {}),
164+
Terminal::Color::TrueColor);
165+
}
166+
167+
TEST(ColorTest, ComputeColorSupportNoColor) {
168+
const ScopedClearEnv no_color("NO_COLOR");
169+
const ScopedClearEnv wt_session("WT_SESSION");
170+
171+
// NO_COLOR (https://no-color.org) disables colors, even on a TrueColor
172+
// capable terminal.
173+
setenv("NO_COLOR", "1", 1);
174+
EXPECT_EQ(Terminal::ComputeColorSupport("xterm", "truecolor", "", "unknown",
175+
"unknown", {}),
176+
Terminal::Color::Palette1);
177+
178+
// NO_COLOR takes precedence over WT_SESSION.
179+
setenv("WT_SESSION", "test_session_id", 1);
180+
EXPECT_EQ(Terminal::ComputeColorSupport("", "", "", "unknown", "unknown", {}),
181+
Terminal::Color::Palette1);
182+
183+
// An empty NO_COLOR is the same as an unset one.
184+
setenv("NO_COLOR", "", 1);
185+
unsetenv("WT_SESSION");
186+
EXPECT_EQ(Terminal::ComputeColorSupport("xterm", "truecolor", "", "unknown",
187+
"unknown", {}),
188+
Terminal::Color::TrueColor);
189+
}
190+
#endif
191+
192+
TEST(ColorTest, FallbackToPalette1) {
193+
Terminal::SetColorSupport(Terminal::Color::Palette1);
194+
// Every color degrades to the terminal's default colors.
195+
EXPECT_EQ(Color(Color::Red).Print(false), "39");
196+
EXPECT_EQ(Color(Color::Red).Print(true), "49");
197+
EXPECT_EQ(Color(Color::DarkRed).Print(false), "39");
198+
EXPECT_EQ(Color::RGB(1, 2, 3).Print(false), "39");
199+
EXPECT_EQ(Color::RGB(1, 2, 3).Print(true), "49");
200+
Terminal::SetColorSupport(Terminal::Color::TrueColor);
201+
}
202+
86203
} // namespace ftxui

src/ftxui/screen/screen.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ void WindowsEmulateVT100Terminal() {
5757
auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
5858

5959
DWORD out_mode = 0;
60-
GetConsoleMode(stdout_handle, &out_mode);
60+
if (!GetConsoleMode(stdout_handle, &out_mode)) {
61+
// The output is not a console (e.g. redirected to a file or a pipe). Keep
62+
// the detected color support and let the consumer of the stream interpret
63+
// the escape sequences.
64+
return;
65+
}
6166

6267
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
6368
const int enable_virtual_terminal_processing = 0x0004;

src/ftxui/screen/terminal.cpp

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,44 @@ Color ComputeColorSupport(std::string_view term,
209209
}
210210

211211
Color TerminalInfo::ComputeColorSupport() const {
212-
// 0. Platform specific overrides.
212+
// TODO(v8): Read NO_COLOR and WT_SESSION from ComputeColorSupportInternal()
213+
// and pass them in as parameters, so that this function remains a pure
214+
// function of its inputs. This requires extending the public
215+
// Terminal::ComputeColorSupport() signature, i.e. an API-breaking change.
216+
217+
// 0. User preference. See https://no-color.org.
218+
const char* no_color = std::getenv("NO_COLOR"); // NOLINT
219+
if (no_color != nullptr && no_color[0] != '\0') {
220+
return Terminal::Color::Palette1;
221+
}
222+
223+
// 1. Platform specific overrides.
213224
#if defined(__EMSCRIPTEN__)
214225
return Terminal::Color::TrueColor;
215226
#endif
227+
#if defined(_WIN32)
228+
// Check if we are running in a console, and if that console supports VT processing.
229+
auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
230+
DWORD out_mode = 0;
231+
if (GetConsoleMode(stdout_handle, &out_mode)) {
232+
const int enable_virtual_terminal_processing = 0x0004;
233+
const int disable_newline_auto_return = 0x0008;
234+
out_mode |= enable_virtual_terminal_processing;
235+
out_mode |= disable_newline_auto_return;
236+
if (!SetConsoleMode(stdout_handle, out_mode)) {
237+
return Terminal::Color::Palette16;
238+
}
239+
}
240+
return Terminal::Color::TrueColor;
241+
#endif
242+
243+
// Check WT_SESSION for Windows Terminal (e.g. when running under WSL).
244+
const char* wt_session = std::getenv("WT_SESSION"); // NOLINT
245+
if (wt_session != nullptr && wt_session[0] != '\0') {
246+
return Terminal::Color::TrueColor;
247+
}
216248

217-
// 1. term / colorterm environment variables.
249+
// 2. term / colorterm environment variables.
218250
if (ContainsAny(impl_->colorterm, {"24bit", "truecolor"})) {
219251
return Terminal::Color::TrueColor;
220252
}
@@ -227,23 +259,27 @@ Color TerminalInfo::ComputeColorSupport() const {
227259
return Terminal::Color::Palette256;
228260
}
229261

230-
// 2. term_program
262+
// 3. term_program
231263
if (ContainsAny(impl_->term_program, {
232264
"iterm",
233-
"apple_terminal",
234265
"vscode",
235266
"warp",
236267
"ghostty",
237268
"wezterm",
238269
})) {
239270
return Terminal::Color::TrueColor;
240271
}
241-
if (Contains(impl_->term_program, "iterm")) {
272+
// Apple's Terminal.app (TERM_PROGRAM=Apple_Terminal) supports 256 colors,
273+
// but not 24bit ones.
274+
if (Contains(impl_->term_program, "apple_terminal")) {
242275
return Terminal::Color::Palette256;
243276
}
244277

245-
// 3. terminal identification.
246-
if (impl_->terminal_emulator_name != "unknown") {
278+
// 4. terminal identification.
279+
// An empty name means the terminal was not identified, the same as
280+
// "unknown".
281+
if (!impl_->terminal_emulator_name.empty() &&
282+
impl_->terminal_emulator_name != "unknown") {
247283
return Terminal::Color::TrueColor;
248284
}
249285
if (impl_->terminal_name == "xterm") {

0 commit comments

Comments
 (0)