6 Commits

Author SHA1 Message Date
Arthur Sonzogni
6c33dd11d6 Merge dc3a6044ee into 994915dbb9 2025-08-06 04:44:10 +00:00
ArthurSonzogni
dc3a6044ee Update 2025-08-06 06:43:59 +02:00
ArthurSonzogni
b7b6a48755 Update 2025-08-03 18:21:14 +02:00
ArthurSonzogni
0cf6f93f77 Refactor FetchTerminalEvent 2025-07-10 15:45:42 +02:00
ArthurSonzogni
4ac28b5675 Add unique_ptr<TaskRunner> 2025-07-10 15:45:42 +02:00
ArthurSonzogni
1b479ee12d Add a task system. 2025-07-10 15:45:41 +02:00
42 changed files with 148 additions and 538 deletions

View File

@@ -2,6 +2,3 @@
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
BasedOnStyle: Chromium
Standard: Cpp11
InsertBraces: true
InsertNewlineAtEOF: true

1
.gitignore vendored
View File

@@ -44,7 +44,6 @@ out/
!doc/**/*.html
!doc/**/*.xml
!doc/**/*.md
!doc/*.md
# examples directory:
!examples/**/*.cpp

View File

@@ -158,17 +158,19 @@ ftxui_cc_library(
"src/ftxui/component/resizable_split.cpp",
"src/ftxui/component/screen_interactive.cpp",
"src/ftxui/component/slider.cpp",
"src/ftxui/component/task.cpp",
"src/ftxui/component/task_internal.hpp",
"src/ftxui/component/task_queue.cpp",
"src/ftxui/component/task_queue.hpp",
"src/ftxui/component/task_runner.cpp",
"src/ftxui/component/task_runner.hpp",
"src/ftxui/component/terminal_input_parser.cpp",
"src/ftxui/component/terminal_input_parser.hpp",
"src/ftxui/component/util.cpp",
"src/ftxui/component/window.cpp",
# Core
"src/ftxui/core/task.cpp",
"src/ftxui/core/task.hpp",
"src/ftxui/core/task_queue.cpp",
"src/ftxui/core/task_queue.hpp",
"src/ftxui/core/task_runner.cpp",
"src/ftxui/core/task_runner.hpp",
# Private header from ftxui:dom.
"src/ftxui/dom/node_decorator.hpp",

View File

@@ -24,7 +24,6 @@ Next
import ftxui.util;
```
Thanks @mikomikotaishi for PR #1015.
- Remove dependency on 'pthread'.
### Component
- Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal

View File

@@ -144,16 +144,16 @@ add_library(component
src/ftxui/component/resizable_split.cpp
src/ftxui/component/screen_interactive.cpp
src/ftxui/component/slider.cpp
src/ftxui/component/task.cpp
src/ftxui/component/task_internal.hpp
src/ftxui/component/task_queue.cpp
src/ftxui/component/task_queue.hpp
src/ftxui/component/task_runner.cpp
src/ftxui/component/task_runner.hpp
src/ftxui/component/terminal_input_parser.cpp
src/ftxui/component/terminal_input_parser.hpp
src/ftxui/component/util.cpp
src/ftxui/component/window.cpp
src/ftxui/core/task.cpp
src/ftxui/core/task.hpp
src/ftxui/core/task_queue.cpp
src/ftxui/core/task_queue.hpp
src/ftxui/core/task_runner.cpp
src/ftxui/core/task_runner.hpp
)
target_link_libraries(dom PUBLIC screen)

View File

@@ -378,8 +378,6 @@ Several games using the FTXUI have been made during the Game Jam:
- [smoothlife](https://github.com/cpp-best-practices/game_jam/blob/main/Jam1_April_2022/smoothlife.md)
- [Consu](https://github.com/cpp-best-practices/game_jam/blob/main/Jam1_April_2022/consu.md)
## Build using CMake
It is **highly** recommended to use CMake FetchContent to depend on FTXUI so you may specify which commit you would like to depend on.

View File

@@ -22,9 +22,9 @@ add_executable(ftxui-tests
src/ftxui/component/resizable_split_test.cpp
src/ftxui/component/screen_interactive_test.cpp
src/ftxui/component/slider_test.cpp
src/ftxui/component/task_test.cpp
src/ftxui/component/terminal_input_parser_test.cpp
src/ftxui/component/toggle_test.cpp
src/ftxui/core/task_test.cpp
src/ftxui/dom/blink_test.cpp
src/ftxui/dom/bold_test.cpp
src/ftxui/dom/border_test.cpp

View File

@@ -1,58 +0,0 @@
# POSIX Piped Input in FTXUI
> [!WARNING]
> This feature works only on Linux and macOS. It is not supported on
> Windows and WebAssembly.
## What is a POSIX Pipe?
A POSIX pipe is a way for two separate programs to communicate. One program sends its output directly as input to another program. Think of it like a one-way tube for data.
**Example:**
Imagine you want to list files and then filter them interactively.
- `ls`: Lists files.
- `interactive_grep`: An FTXUI application that filters text and lets you type.
You can connect them with a pipe (`|`):
```bash
ls -l | interactive_grep
```
Here's what happens:
1. `ls -l` lists files with details.
2. The `|` sends this list directly to `interactive_grep`.
3. `interactive_grep` receives the list and displays it. Because it's an FTXUI app, you can then type to filter the list, even though it received initial data from `ls`.
## How FTXUI Handles Piped Input
Now that you understand what a POSIX pipe is, let's look at how FTXUI uses them.
FTXUI lets your application read data from other programs (like from a pipe) while still allowing you to use your keyboard for interaction. This is useful for interactive command-line tools that process data.
Normally, FTXUI applications receive all input from `stdin`. However, when FTXUI detects that `stdin` is connected to the output of a pipe (meaning data is being piped into your application), it automatically switches to reading interactive keyboard input from `/dev/tty`. This ensures that your application can still receive user input even while processing piped data.
This feature is **turned on by default**.
If your FTXUI application needs to read piped data and also respond to keyboard input, you typically don't need to do anything special:
```cpp
auto screen = ScreenInteractive::Fullscreen();
// screen.HandlePipedInput(true); // This is enabled by default
screen.Loop(component);
```
## Turning Off Piped Input
If you don't need this feature, or if it conflicts with your custom input handling, you can turn it off.
To disable it, call `HandlePipedInput(false)` before starting your application's main loop:
```cpp
auto screen = ScreenInteractive::Fullscreen();
screen.HandlePipedInput(false); // Turn off piped input handling
screen.Loop(component);
```

View File

@@ -133,9 +133,8 @@ int main() {
float dy = 50.f;
ys[x] = int(dy + 20 * cos(dx * 0.14) + 10 * sin(dx * 0.42));
}
for (int x = 1; x < 99; x++) {
for (int x = 1; x < 99; x++)
c.DrawPointLine(x, ys[x], x + 1, ys[x + 1]);
}
return canvas(std::move(c));
});

View File

@@ -82,12 +82,10 @@ int main() {
size(WIDTH, EQUAL, dimx) | size(HEIGHT, EQUAL, dimy) |
bgcolor(Color::HSV(index * 25, 255, 255)) |
color(Color::Black);
if (element_xflex_grow) {
if (element_xflex_grow)
element = element | xflex_grow;
}
if (element_yflex_grow) {
if (element_yflex_grow)
element = element | yflex_grow;
}
return element;
};
@@ -121,12 +119,10 @@ int main() {
group = group | notflex;
if (!group_xflex_grow) {
if (!group_xflex_grow)
group = hbox(group, filler());
}
if (!group_yflex_grow) {
if (!group_yflex_grow)
group = vbox(group, filler());
}
group = group | flex;
return group;

View File

@@ -1,11 +1,11 @@
// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <stddef.h> // for size_t
#include <array> // for array
#include <atomic> // for atomic
#include <chrono> // for operator""s, chrono_literals
#include <cmath> // for sin
#include <stddef.h> // for size_t
#include <array> // for array
#include <atomic> // for atomic
#include <chrono> // for operator""s, chrono_literals
#include <cmath> // for sin
#include <ftxui/component/loop.hpp>
#include <functional> // for ref, reference_wrapper, function
#include <memory> // for allocator, shared_ptr, __shared_ptr_access
@@ -514,7 +514,7 @@ int main() {
});
Loop loop(&screen, main_renderer);
while (!loop.HasQuitted()) {
while(!loop.HasQuitted()) {
// Update the state of the application.
shift++;
@@ -525,7 +525,7 @@ int main() {
loop.RunOnce();
// Sleep for a short duration to control the frame rate (60 FPS).
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 60));
std::this_thread::sleep_for(std::chrono::milliseconds(1000/60));
}
return 0;

View File

@@ -22,12 +22,10 @@ MenuEntryOption Colored(ftxui::Color c) {
option.transform = [c](EntryState state) {
state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label) | color(c);
if (state.focused) {
if (state.focused)
e = e | inverted;
}
if (state.active) {
if (state.active)
e = e | bold;
}
return e;
};
return option;

View File

@@ -17,9 +17,8 @@ int main() {
std::vector<std::string> entries;
int selected = 0;
for (int i = 0; i < 30; ++i) {
for (int i = 0; i < 30; ++i)
entries.push_back("Entry " + std::to_string(i));
}
auto radiobox = Menu(&entries, &selected);
auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame |

View File

@@ -17,9 +17,8 @@ int main() {
std::vector<std::string> entries;
int selected = 0;
for (int i = 0; i < 100; ++i) {
for (int i = 0; i < 100; ++i)
entries.push_back(std::to_string(i));
}
auto radiobox = Menu(&entries, &selected, MenuOption::Horizontal());
auto renderer = Renderer(
radiobox, [&] { return radiobox->Render() | hscroll_indicator | frame; });

View File

@@ -116,12 +116,10 @@ Component VMenu1(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) {
state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label);
if (state.focused) {
if (state.focused)
e = e | bgcolor(Color::Blue);
}
if (state.active) {
if (state.active)
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -132,12 +130,10 @@ Component VMenu2(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) {
state.label += (state.active ? " <" : " ");
Element e = hbox(filler(), text(state.label));
if (state.focused) {
if (state.focused)
e = e | bgcolor(Color::Red);
}
if (state.active) {
if (state.active)
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -148,16 +144,13 @@ Component VMenu3(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) {
Element e = state.active ? text("[" + state.label + "]")
: text(" " + state.label + " ");
if (state.focused) {
if (state.focused)
e = e | bold;
}
if (state.focused) {
if (state.focused)
e = e | color(Color::Blue);
}
if (state.active) {
if (state.active)
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -252,12 +245,10 @@ Component HMenu5(std::vector<std::string>* entries, int* selected) {
animation::easing::ElasticOut);
option.entries_option.transform = [](EntryState state) {
Element e = text(state.label) | hcenter | flex;
if (state.active && state.focused) {
if (state.active && state.focused)
e = e | bold;
}
if (!state.focused && !state.active) {
if (!state.focused && !state.active)
e = e | dim;
}
return e;
};
option.underline.color_inactive = Color::Default;

View File

@@ -20,9 +20,8 @@ using namespace ftxui;
Component DummyComponent(int id) {
return Renderer([id](bool focused) {
auto t = text("component " + std::to_string(id));
if (focused) {
if (focused)
t = t | inverted;
}
return t;
});
}

View File

@@ -17,9 +17,8 @@ int main() {
std::vector<std::string> entries;
int selected = 0;
for (int i = 0; i < 30; ++i) {
for (int i = 0; i < 30; ++i)
entries.push_back("RadioBox " + std::to_string(i));
}
auto radiobox = Radiobox(&entries, &selected);
auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame |

View File

@@ -19,11 +19,10 @@ int main() {
// 1. Example of focusable renderer:
auto renderer_focusable = Renderer([](bool focused) {
if (focused) {
if (focused)
return text("FOCUSABLE RENDERER()") | center | bold | border;
} else {
else
return text(" Focusable renderer() ") | center | border;
}
});
// 2. Examples of a non focusable renderer.
@@ -34,11 +33,10 @@ int main() {
// 3. Renderer can wrap other components to redefine their Render() function.
auto button = Button("Wrapped quit button", screen.ExitLoopClosure());
auto renderer_wrap = Renderer(button, [&] {
if (button->Focused()) {
if (button->Focused())
return button->Render() | bold | color(Color::Red);
} else {
else
return button->Render();
}
});
// Let's renderer everyone:

View File

@@ -32,12 +32,10 @@ int main() {
// Plot a function:
std::vector<int> ys(100);
for (int x = 0; x < 100; x++) {
for (int x = 0; x < 100; x++)
ys[x] = int(80 + 20 * cos(x * 0.2));
}
for (int x = 0; x < 99; x++) {
for (int x = 0; x < 99; x++)
c.DrawPointLine(x, ys[x], x + 1, ys[x + 1], Color::Red);
}
auto document = canvas(&c) | border;

View File

@@ -86,9 +86,8 @@ int main() {
auto render = [&]() {
std::vector<Element> entries;
for (auto& task : displayed_task) {
for (auto& task : displayed_task)
entries.push_back(renderTask(task));
}
return vbox({
// List of tasks.
@@ -139,9 +138,8 @@ int main() {
std::this_thread::sleep_for(0.01s);
// Exit
if (nb_active + nb_queued == 0) {
if (nb_active + nb_queued == 0)
break;
}
// Update the model for the next frame.
updateModel();

View File

@@ -21,9 +21,8 @@ int main() {
for (int index = 0; index < 200; ++index) {
std::vector<Element> entries;
for (int i = 0; i < 23; ++i) {
if (i != 0) {
if (i != 0)
entries.push_back(separator());
}
entries.push_back( //
hbox({
text(std::to_string(i)) | size(WIDTH, EQUAL, 2),

View File

@@ -8,7 +8,6 @@
#include <memory> // for make_shared, shared_ptr
#include <utility> // for forward
#include <ftxui/util/warn_windows_macro.hpp>
#include "ftxui/component/component_base.hpp" // for Component, Components
#include "ftxui/component/component_options.hpp" // for ButtonOption, CheckboxOption, MenuOption
#include "ftxui/dom/elements.hpp" // for Element

View File

@@ -9,9 +9,8 @@
#include <ftxui/dom/direction.hpp> // for Direction, Direction::Left, Direction::Right, Direction::Down
#include <ftxui/dom/elements.hpp> // for Element, separator
#include <ftxui/util/ref.hpp> // for Ref, ConstRef, StringRef
#include <ftxui/util/warn_windows_macro.hpp>
#include <functional> // for function
#include <string> // for string
#include <functional> // for function
#include <string> // for string
#include "ftxui/component/component_base.hpp" // for Component
#include "ftxui/screen/color.hpp" // for Color, Color::GrayDark, Color::White

View File

@@ -4,7 +4,6 @@
#ifndef FTXUI_COMPONENT_RECEIVER_HPP_
#define FTXUI_COMPONENT_RECEIVER_HPP_
#include <ftxui/util/warn_windows_macro.h>
#include <algorithm> // for copy, max
#include <atomic> // for atomic, __atomic_base
#include <condition_variable> // for condition_variable

View File

@@ -4,10 +4,10 @@
#ifndef FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#include <atomic> // for atomic
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#include <atomic> // for atomic
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -25,7 +25,7 @@ using Component = std::shared_ptr<ComponentBase>;
class ScreenInteractivePrivate;
namespace task {
class TaskRunner;
class TaskRunner;
}
/// @brief ScreenInteractive is a `Screen` that can handle events, run a main
@@ -43,11 +43,10 @@ class ScreenInteractive : public Screen {
static ScreenInteractive TerminalOutput();
// Destructor.
~ScreenInteractive() override;
~ScreenInteractive();
// Options. Must be called before Loop().
void TrackMouse(bool enable = true);
void HandlePipedInput(bool enable = true);
// Return the currently active screen, nullptr if none.
static ScreenInteractive* Active();
@@ -101,8 +100,6 @@ class ScreenInteractive : public Screen {
void Draw(Component component);
void ResetCursorPosition();
void InstallPipedInputHandling();
void Signal(int signal);
void FetchTerminalEvents();
@@ -120,7 +117,6 @@ class ScreenInteractive : public Screen {
int dimx,
int dimy,
bool use_alternative_screen);
const Dimension dimension_;
const bool use_alternative_screen_;
@@ -145,9 +141,6 @@ class ScreenInteractive : public Screen {
bool force_handle_ctrl_c_ = true;
bool force_handle_ctrl_z_ = true;
// Piped input handling state (POSIX only)
bool handle_piped_input_ = true;
// The style of the cursor to restore on exit.
int cursor_reset_shape_ = 1;
@@ -175,6 +168,7 @@ class ScreenInteractive : public Screen {
Component component_;
public:
class Private {
public:

View File

@@ -1,18 +0,0 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#define FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#ifdef min
#error \
"The macro 'min' is defined, which conflicts with the standard C++ library and FTXUI. This is often caused by including <windows.h>. To fix this, add '#define NOMINMAX' before including <windows.h>, or pass '/DNOMINMAX' as a compiler flag."
#endif
#ifdef max
#error \
"The macro 'max' is defined, which conflicts with the standard C++ library and FTXUI. This is often caused by including <windows.h>. To fix this, add '#define NOMINMAX' before including <windows.h>, or pass '/DNOMINMAX' as a compiler flag."
#endif
#endif // FTXUI_UTIL_WARN_WINDOWS_MACRO_H_

View File

@@ -23,9 +23,8 @@ bool GeneratorBool(const char*& data, size_t& size) {
std::string GeneratorString(const char*& data, size_t& size) {
int index = 0;
while (index < size && data[index]) {
while (index < size && data[index])
++index;
}
auto out = std::string(data, data + index);
data += index;
@@ -41,9 +40,8 @@ std::string GeneratorString(const char*& data, size_t& size) {
}
int GeneratorInt(const char* data, size_t size) {
if (size == 0) {
if (size == 0)
return 0;
}
auto out = int(data[0]);
data++;
size--;
@@ -115,9 +113,8 @@ Components GeneratorComponents(const char*& data, size_t& size, int depth);
Component GeneratorComponent(const char*& data, size_t& size, int depth) {
depth--;
int value = GeneratorInt(data, size);
if (depth <= 0) {
if (depth <= 0)
return Button(GeneratorString(data, size), [] {});
}
constexpr int value_max = 19;
value = (value % value_max + value_max) % value_max;

View File

@@ -4,6 +4,7 @@
#include "ftxui/component/screen_interactive.hpp"
#include <algorithm> // for copy, max, min
#include <array> // for array
#include <array>
#include <atomic>
#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
@@ -17,22 +18,27 @@
#include <memory>
#include <stack> // for stack
#include <string>
#include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore
#include <utility> // for move, swap
#include <variant> // for visit, variant
#include <vector> // for vector
#include <thread> // for thread, sleep_for
#include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore
#include <type_traits> // for decay_t
#include <utility> // for move, swap
#include <variant> // for visit, variant
#include <vector> // for vector
#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/loop.hpp" // for Loop
#include "ftxui/component/task_runner.hpp"
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
#include "ftxui/dom/node.hpp" // for Node, Render
#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
#include "ftxui/screen/util.hpp" // for util::clamp
#include "ftxui/util/autoreset.hpp" // for AutoReset
#include "ftxui/core/task_runner.hpp"
#include "ftxui/dom/node.hpp" // for Node, Render
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/screen/pixel.hpp" // for Pixel
#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
#include "ftxui/screen/util.hpp" // for util::clamp
#include "ftxui/util/autoreset.hpp" // for AutoReset
#if defined(_WIN32)
#define DEFINE_CONSOLEV2_PROPERTIES
@@ -372,18 +378,6 @@ void ScreenInteractive::TrackMouse(bool enable) {
track_mouse_ = enable;
}
/// @brief Enable or disable automatic piped input handling.
/// When enabled, FTXUI will detect piped input and redirect stdin from /dev/tty
/// for keyboard input, allowing applications to read piped data while still
/// receiving interactive keyboard events.
/// @param enable Whether to enable piped input handling. Default is true.
/// @note This must be called before Loop().
/// @note This feature is enabled by default.
/// @note This feature is only available on POSIX systems (Linux/macOS).
void ScreenInteractive::HandlePipedInput(bool enable) {
handle_piped_input_ = enable;
}
/// @brief Add a task to the main loop.
/// It will be executed later, after every other scheduled tasks.
void ScreenInteractive::Post(Task task) {
@@ -590,15 +584,15 @@ void ScreenInteractive::Install() {
SetConsoleMode(stdin_handle, in_mode);
SetConsoleMode(stdout_handle, out_mode);
#else // POSIX (Linux & Mac)
// #if defined(__EMSCRIPTEN__)
#else // POSIX (Linux & Mac)
//#if defined(__EMSCRIPTEN__)
//// Reading stdin isn't blocking.
// int flags = fcntl(0, F_GETFL, 0);
// fcntl(0, F_SETFL, flags | O_NONBLOCK);
//int flags = fcntl(0, F_GETFL, 0);
//fcntl(0, F_SETFL, flags | O_NONBLOCK);
//// Restore the terminal configuration on exit.
// on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
// #endif
//on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
//#endif
for (const int signal : {SIGWINCH, SIGTSTP}) {
InstallSignalHandler(signal);
}
@@ -670,55 +664,11 @@ void ScreenInteractive::Install() {
// ensure it is fully applied:
Flush();
// Redirect the true terminal to stdin, so that we can read keyboard input
// directly from stdin, even if the input is piped from a file or another
// process.
//
// TODO: Instead of redirecting stdin, we could define the file descriptor to
// read from, and use it in the TerminalInputParser.
InstallPipedInputHandling();
quit_ = false;
PostAnimationTask();
}
void ScreenInteractive::InstallPipedInputHandling() {
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
// Handle piped input redirection if explicitly enabled by the application.
// This allows applications to read data from stdin while still receiving
// keyboard input from the terminal for interactive use.
if (!handle_piped_input_) {
return;
}
// If stdin is a terminal, we don't need to redirect it.
if (isatty(STDIN_FILENO)) {
return;
}
// Save the current stdin so we can restore it later.
int original_fd = dup(STDIN_FILENO);
if (original_fd < 0) {
return;
}
// Redirect stdin to the controlling terminal for keyboard input.
if (std::freopen("/dev/tty", "r", stdin) == nullptr) {
// Failed to open /dev/tty (containers, headless systems, etc.)
// Clean up and continue without redirection
close(original_fd);
return;
}
// Restore the original stdin file descriptor on exit.
on_exit_functions.emplace([=] {
dup2(original_fd, STDIN_FILENO);
close(original_fd);
});
#endif
}
// private
void ScreenInteractive::Uninstall() {
ExitNow();
@@ -735,8 +685,8 @@ void ScreenInteractive::RunOnceBlocking(Component component) {
size_t executed_task = internal_->task_runner.ExecutedTasks();
// Wait for at least one task to execute.
while (executed_task == internal_->task_runner.ExecutedTasks() &&
!HasQuitted()) {
while(executed_task == internal_->task_runner.ExecutedTasks() &&
!HasQuitted()) {
RunOnce(component);
const auto now = std::chrono::steady_clock::now();
@@ -783,7 +733,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
[&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
// clang-format off
// clang-format off
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
@@ -1072,23 +1022,23 @@ void ScreenInteractive::Signal(int signal) {
void ScreenInteractive::FetchTerminalEvents() {
#if defined(_WIN32)
auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
auto get_input_records = [&] {
// Check if there is input in the console.
auto console = GetStdHandle(STD_INPUT_HANDLE);
DWORD number_of_events = 0;
if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
return std::vector<INPUT_RECORD>();
return;
}
if (number_of_events <= 0) {
// No input, return.
return std::vector<INPUT_RECORD>();
return;
}
// Read the input events.
std::vector<INPUT_RECORD> records(number_of_events);
DWORD number_of_events_read = 0;
if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
&number_of_events_read)) {
return std::vector<INPUT_RECORD>();
return;
}
records.resize(number_of_events_read);
return records;
@@ -1099,7 +1049,7 @@ void ScreenInteractive::FetchTerminalEvents() {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
const size_t timeout_microseconds =
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
internal_->terminal_input_parser.Timeout(timeout_microseconds);
return;
}
@@ -1113,9 +1063,8 @@ void ScreenInteractive::FetchTerminalEvents() {
case KEY_EVENT: {
auto key_event = r.Event.KeyEvent;
// ignore UP key events
if (key_event.bKeyDown == FALSE) {
if (key_event.bKeyDown == FALSE)
continue;
}
std::wstring wstring;
wstring += key_event.uChar.UnicodeChar;
for (auto it : to_string(wstring)) {
@@ -1141,17 +1090,17 @@ void ScreenInteractive::FetchTerminalEvents() {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
const size_t timeout_microseconds =
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
internal_->terminal_input_parser.Timeout(timeout_microseconds);
return;
}
internal_->last_char_time = std::chrono::steady_clock::now();
// Convert the chars to events.
for (size_t i = 0; i < l; ++i) {
for(size_t i = 0; i < l; ++i) {
internal_->terminal_input_parser.Add(out[i]);
}
#else // POSIX (Linux & Mac)
#else // POSIX (Linux & Mac)
if (!CheckStdinReady()) {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
@@ -1167,7 +1116,7 @@ void ScreenInteractive::FetchTerminalEvents() {
size_t l = read(fileno(stdin), out.data(), out.size());
// Convert the chars to events.
for (size_t i = 0; i < l; ++i) {
for(size_t i = 0; i < l; ++i) {
internal_->terminal_input_parser.Add(out[i]);
}
#endif
@@ -1178,8 +1127,8 @@ void ScreenInteractive::PostAnimationTask() {
// Repeat the animation task every 15ms. This correspond to a frame rate
// of around 66fps.
internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
std::chrono::milliseconds(15));
internal_->task_runner.PostDelayedTask(
[this] { PostAnimationTask(); }, std::chrono::milliseconds(15));
}
bool ScreenInteractive::SelectionData::operator==(

View File

@@ -1,220 +0,0 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <gtest/gtest.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <sys/stat.h>
#include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/dom/elements.hpp"
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
namespace ftxui {
namespace {
// Test fixture for piped input functionality
class PipedInputTest : public ::testing::Test {
protected:
void SetUp() override {
// Save original stdin for restoration
original_stdin_ = dup(STDIN_FILENO);
}
void TearDown() override {
// Restore original stdin
if (original_stdin_ >= 0) {
dup2(original_stdin_, STDIN_FILENO);
close(original_stdin_);
}
}
// Create a pipe and redirect stdin to read from it
void SetupPipedStdin() {
if (pipe(pipe_fds_) == 0) {
dup2(pipe_fds_[0], STDIN_FILENO);
close(pipe_fds_[0]);
// Keep write end open for writing test data
piped_stdin_setup_ = true;
}
}
// Write test data to the piped stdin
void WriteToPipedStdin(const std::string& data) {
if (piped_stdin_setup_) {
write(pipe_fds_[1], data.c_str(), data.length());
close(pipe_fds_[1]); // Close write end to signal EOF
}
}
// Check if /dev/tty is available (not available in some CI environments)
bool IsTtyAvailable() {
struct stat st;
return stat("/dev/tty", &st) == 0;
}
private:
int original_stdin_ = -1;
int pipe_fds_[2] = {-1, -1};
bool piped_stdin_setup_ = false;
};
TEST_F(PipedInputTest, DefaultBehaviorEnabled) {
// Test that HandlePipedInput is enabled by default
if (!IsTtyAvailable()) {
GTEST_SKIP() << "/dev/tty not available in this environment";
}
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
// Install should redirect stdin since HandlePipedInput is on by default
screen.Install();
// Stdin should be the tty
EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall();
}
TEST_F(PipedInputTest, ExplicitlyDisabled) {
// Test that explicitly disabling works
auto screen = ScreenInteractive::TerminalOutput();
screen.HandlePipedInput(false);
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
screen.Install();
// Stdin should still be the pipe since feature is disabled
EXPECT_FALSE(isatty(STDIN_FILENO));
screen.Uninstall();
}
TEST_F(PipedInputTest, ExplicitlyEnabled) {
if (!IsTtyAvailable()) {
GTEST_SKIP() << "/dev/tty not available in this environment";
}
auto screen = ScreenInteractive::TerminalOutput();
screen.HandlePipedInput(true); // Explicitly enable
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
// Before install: stdin should be piped
EXPECT_FALSE(isatty(STDIN_FILENO));
screen.Install();
// After install with piped input handling: stdin should be redirected to tty
EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall();
// After uninstall: stdin should be restored to original state
// Note: This will be the pipe we set up, so it should be non-tty
EXPECT_FALSE(isatty(STDIN_FILENO));
}
TEST_F(PipedInputTest, NormalStdinUnchanged) {
// Test that normal stdin (not piped) is not affected
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
// Don't setup piped stdin - use normal stdin
bool original_isatty = isatty(STDIN_FILENO);
screen.Install();
// Stdin should remain unchanged
EXPECT_EQ(original_isatty, isatty(STDIN_FILENO));
screen.Uninstall();
// Stdin should still be unchanged
EXPECT_EQ(original_isatty, isatty(STDIN_FILENO));
}
TEST_F(PipedInputTest, MultipleInstallUninstallCycles) {
if (!IsTtyAvailable()) {
GTEST_SKIP() << "/dev/tty not available in this environment";
}
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
// First cycle
screen.Install();
EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall();
EXPECT_FALSE(isatty(STDIN_FILENO));
// Second cycle should work the same
screen.Install();
EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall();
EXPECT_FALSE(isatty(STDIN_FILENO));
}
TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) {
auto screen = ScreenInteractive::TerminalOutput();
// Test method can be called multiple times
screen.HandlePipedInput(true);
screen.HandlePipedInput(false);
screen.HandlePipedInput(true);
// Should be enabled after last call
SetupPipedStdin();
WriteToPipedStdin("test data\n");
if (IsTtyAvailable()) {
screen.Install();
EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall();
}
}
// Test the graceful fallback when /dev/tty is not available
// This test simulates environments like containers where /dev/tty might not exist
TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) {
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
// This test doesn't directly mock /dev/tty unavailability since that's hard to do
// in a unit test environment, but the code path handles freopen() failure gracefully
screen.Install();
// The behavior depends on whether /dev/tty is available
// If available, stdin gets redirected; if not, it remains piped
// Both behaviors are correct
screen.Uninstall();
// After uninstall, stdin should be restored
EXPECT_FALSE(isatty(STDIN_FILENO)); // Should still be our test pipe
}
} // namespace
} // namespace ftxui
#endif // !defined(_WIN32) && !defined(__EMSCRIPTEN__)

View File

@@ -28,9 +28,8 @@ namespace {
class StdCapture {
public:
explicit StdCapture(std::string* captured) : captured_(captured) {
if (pipe(pipefd_) != 0) {
if (pipe(pipefd_) != 0)
return;
}
old_stdout_ = dup(fileno(stdout));
fflush(stdout);
dup2(pipefd_[1], fileno(stdout));

View File

@@ -152,8 +152,8 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
case CURSOR_POSITION:
out_(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
pending_.clear();
return;

View File

@@ -17,19 +17,16 @@ namespace ftxui {
// Test char |c| to are trivially converted into |Event::Character(c)|.
TEST(Event, Character) {
std::vector<char> basic_char;
for (char c = 'a'; c <= 'z'; ++c) {
for (char c = 'a'; c <= 'z'; ++c)
basic_char.push_back(c);
}
for (char c = 'A'; c <= 'Z'; ++c) {
for (char c = 'A'; c <= 'Z'; ++c)
basic_char.push_back(c);
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
for (char c : basic_char) {
for (char c : basic_char)
parser.Add(c);
}
for (size_t i = 0; i < basic_char.size(); ++i) {
EXPECT_TRUE(received_events[i].is_character());
@@ -288,9 +285,8 @@ TEST(Event, UTF8) {
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
for (auto input : test.input) {
for (auto input : test.input)
parser.Add(input);
}
if (test.valid) {
EXPECT_EQ(1, received_events.size());
@@ -319,9 +315,8 @@ TEST(Event, Control) {
};
std::vector<TestCase> cases;
for (int i = 0; i < 32; ++i) {
if (i == 8 || i == 13 || i == 24 || i == 26 || i == 27) {
if (i == 8 || i == 13 || i == 24 || i == 26 || i == 27)
continue;
}
cases.push_back({char(i), false});
}
cases.push_back({char(24), false});
@@ -346,9 +341,8 @@ TEST(Event, Control) {
TEST(Event, Special) {
auto str = [](std::string input) {
std::vector<unsigned char> output;
for (auto it : input) {
for (auto it : input)
output.push_back(it);
}
return output;
};
@@ -357,12 +351,9 @@ TEST(Event, Special) {
Event expected;
} kTestCase[] = {
// Arrow (default cursor mode)
{str(""), Event::ArrowUp},
{str(""), Event::ArrowDown},
{str(""), Event::ArrowRight},
{str(""), Event::ArrowLeft},
{str(""), Event::Home},
{str(""), Event::End},
{str(""), Event::ArrowUp}, {str(""), Event::ArrowDown},
{str(""), Event::ArrowRight}, {str(""), Event::ArrowLeft},
{str(""), Event::Home}, {str(""), Event::End},
// Arrow (application cursor mode)
{str("\x1BOA"), Event::ArrowUp},

View File

@@ -1,7 +1,7 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/component/task_internal.hpp"
#include "task.hpp"
namespace ftxui::task {
bool PendingTask::operator<(const PendingTask& other) const {
@@ -16,4 +16,5 @@ bool PendingTask::operator<(const PendingTask& other) const {
}
return time.value() > other.time.value();
}
} // namespace ftxui::task
} // namespace ftxui::task

View File

@@ -21,7 +21,8 @@ struct PendingTask {
// Delayed task with a duration
PendingTask(Task t, std::chrono::steady_clock::duration duration)
: task(std::move(t)), time(std::chrono::steady_clock::now() + duration) {}
: task(std::move(t)),
time(std::chrono::steady_clock::now() + duration) {}
/// The task to be executed.
Task task;
@@ -35,6 +36,7 @@ struct PendingTask {
bool operator<(const PendingTask& other) const;
};
} // namespace ftxui::task
} // namespace ftxui::task
#endif // TASK_HPP_

View File

@@ -1,7 +1,7 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/component/task_queue.hpp"
#include "task_queue.hpp"
namespace ftxui::task {
@@ -50,4 +50,5 @@ auto TaskQueue::Get() -> MaybeTask {
return std::monostate{};
}
} // namespace ftxui::task
} // namespace ftxui::task

View File

@@ -7,7 +7,7 @@
#include <queue>
#include <variant>
#include "ftxui/component/task_internal.hpp" // for PendingTask, Task
#include "task.hpp"
namespace ftxui::task {
@@ -25,13 +25,16 @@ struct TaskQueue {
std::variant<Task, std::chrono::steady_clock::duration, std::monostate>;
auto Get() -> MaybeTask;
bool HasImmediateTasks() const { return !immediate_tasks_.empty(); }
bool HasImmediateTasks() const {
return !immediate_tasks_.empty();
}
private:
std::queue<PendingTask> immediate_tasks_;
std::priority_queue<PendingTask> delayed_tasks_;
};
} // namespace ftxui::task
} // namespace ftxui::task
#endif

View File

@@ -1,7 +1,7 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/component/task_runner.hpp"
#include "task_runner.hpp"
#include <cassert>
#include <thread>
@@ -20,6 +20,7 @@ TaskRunner::~TaskRunner() {
current_task_runner = previous_task_runner_;
}
// static
auto TaskRunner::Current() -> TaskRunner* {
assert(current_task_runner);
@@ -72,4 +73,5 @@ auto TaskRunner::Run() -> void {
}
}
} // namespace ftxui::task
} // namespace ftxui::task

View File

@@ -4,8 +4,8 @@
#ifndef TASK_RUNNER_HPP
#define TASK_RUNNER_HPP
#include "ftxui/component/task_internal.hpp"
#include "ftxui/component/task_queue.hpp"
#include "task.hpp"
#include "task_queue.hpp"
namespace ftxui::task {
@@ -21,8 +21,8 @@ class TaskRunner {
auto PostTask(Task task) -> void;
/// Schedules a task to be executed after a certain duration.
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration)
-> void;
auto PostDelayedTask(Task task,
std::chrono::steady_clock::duration duration) -> void;
/// Runs the tasks in the queue, return the delay until the next delayed task
/// can be executed.
@@ -31,7 +31,9 @@ class TaskRunner {
// Runs the tasks in the queue, blocking until all tasks are executed.
auto Run() -> void;
bool HasImmediateTasks() const { return queue_.HasImmediateTasks(); }
bool HasImmediateTasks() const {
return queue_.HasImmediateTasks();
}
size_t ExecutedTasks() const { return executed_tasks_; }
@@ -41,6 +43,7 @@ class TaskRunner {
size_t executed_tasks_ = 0;
};
} // namespace ftxui::task
} // namespace ftxui::task
#endif // TASK_RUNNER_HPP

View File

@@ -4,12 +4,12 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/component/task_internal.hpp"
#include "task.hpp"
#include <gtest/gtest.h>
#include <thread> // for sleep_for
#include "ftxui/component/task_runner.hpp"
#include "task_runner.hpp"
namespace ftxui::task {
@@ -91,4 +91,5 @@ TEST(TaskTest, RunDelayedTask) {
EXPECT_EQ(values, (std::vector<int>{1, 2, 3}));
}
} // namespace ftxui::task
} // namespace ftxui::task

View File

@@ -47,9 +47,8 @@ namespace {
#if defined(_WIN32)
void WindowsEmulateVT100Terminal() {
static bool done = false;
if (done) {
if (done)
return;
}
done = true;
// Enable VT processing on stdout and stdin

View File

@@ -1284,9 +1284,8 @@ bool IsCombining(uint32_t ucs) {
}
bool IsFullWidth(uint32_t ucs) {
if (ucs < 0x0300) { // Quick path: // NOLINT
if (ucs < 0x0300) // Quick path: // NOLINT
return false;
}
return Bisearch(ucs, g_full_width_characters);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.