Add opt-in piped input support for POSIX systems

Enables applications to read piped data while maintaining interactive
keyboard input by redirecting stdin to /dev/tty when explicitly enabled.
This commit is contained in:
Harri Pehkonen 2025-08-09 20:26:37 -07:00 committed by ArthurSonzogni
parent 346f751527
commit e0e6964968
No known key found for this signature in database
GPG Key ID: 41D98248C074CD6C
4 changed files with 294 additions and 0 deletions

View File

@ -378,6 +378,22 @@ 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)
## Advanced Usage
### Piped Input Support
If your application reads from stdin (piped data) and also needs interactive keyboard input:
```cpp
auto screen = ScreenInteractive::Fullscreen();
screen.HandlePipedInput(true); // Enable before Loop()
screen.Loop(component);
```
This allows commands like `cat data.txt | your_app` to work with full keyboard interaction.
**Note:** This feature is only available on POSIX systems (Linux/macOS). On Windows, the method call is a no-op.
## 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

@ -47,6 +47,7 @@ class ScreenInteractive : public Screen {
// 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();
@ -141,6 +142,13 @@ class ScreenInteractive : public Screen {
bool force_handle_ctrl_c_ = true;
bool force_handle_ctrl_z_ = true;
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
// Piped input handling state (POSIX only)
bool handle_piped_input_ = false;
bool stdin_was_redirected_ = false;
int original_stdin_fd_ = -1;
#endif
// The style of the cursor to restore on exit.
int cursor_reset_shape_ = 1;

View File

@ -372,6 +372,24 @@ 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 to /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
/// @note This must be called before Loop().
/// @note This feature is disabled by default for backward compatibility.
/// @note This feature is only available on POSIX systems (Linux/macOS).
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
void ScreenInteractive::HandlePipedInput(bool enable) {
handle_piped_input_ = enable;
}
#else
void ScreenInteractive::HandlePipedInput(bool /*enable*/) {
// This feature is not supported on this platform.
}
#endif
/// @brief Add a task to the main loop.
/// It will be executed later, after every other scheduled tasks.
void ScreenInteractive::Post(Task task) {
@ -658,6 +676,28 @@ void ScreenInteractive::Install() {
// ensure it is fully applied:
Flush();
#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_ && !stdin_was_redirected_ && !isatty(STDIN_FILENO)) {
// Save the current stdin so we can restore it later
original_stdin_fd_ = dup(STDIN_FILENO);
if (original_stdin_fd_ >= 0) {
// Redirect stdin to the controlling terminal for keyboard input
if (freopen("/dev/tty", "r", stdin) != nullptr) {
stdin_was_redirected_ = true;
} else {
// Failed to open /dev/tty (containers, headless systems, etc.)
// Clean up and continue without redirection
close(original_stdin_fd_);
original_stdin_fd_ = -1;
}
}
// If dup() failed, we silently continue without redirection
}
#endif
quit_ = false;
PostAnimationTask();
@ -666,6 +706,17 @@ void ScreenInteractive::Install() {
// private
void ScreenInteractive::Uninstall() {
ExitNow();
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
// Restore stdin to its original state if we redirected it
if (stdin_was_redirected_ && original_stdin_fd_ >= 0) {
dup2(original_stdin_fd_, STDIN_FILENO);
close(original_stdin_fd_);
original_stdin_fd_ = -1;
stdin_was_redirected_ = false;
}
#endif
OnExit();
}

View File

@ -0,0 +1,219 @@
// 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, DefaultBehaviorNoChange) {
// Test that HandlePipedInput is disabled by default
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
WriteToPipedStdin("test data\n");
// Install should not redirect stdin since HandlePipedInput not called
screen.Install();
// Stdin should still be the pipe (isatty should return false)
EXPECT_FALSE(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, PipedInputDetectionAndRedirection) {
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();
screen.HandlePipedInput(true);
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();
screen.HandlePipedInput(true);
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();
screen.HandlePipedInput(true);
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__)