From def5c6d50cabb3cfadfd0470daed7b6208aa0705 Mon Sep 17 00:00:00 2001 From: Harri Pehkonen Date: Sat, 9 Aug 2025 20:26:37 -0700 Subject: [PATCH 1/6] 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. --- README.md | 16 ++ .../ftxui/component/screen_interactive.hpp | 8 + src/ftxui/component/screen_interactive.cpp | 51 ++++ .../screen_interactive_piped_input_test.cpp | 219 ++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 src/ftxui/component/screen_interactive_piped_input_test.cpp diff --git a/README.md b/README.md index e5eba35e6..fdfdd28ca 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index f8b899f3b..0291e76e3 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -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; diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index c2341cdcf..fad541130 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -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(); } diff --git a/src/ftxui/component/screen_interactive_piped_input_test.cpp b/src/ftxui/component/screen_interactive_piped_input_test.cpp new file mode 100644 index 000000000..e953bbd30 --- /dev/null +++ b/src/ftxui/component/screen_interactive_piped_input_test.cpp @@ -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 +#include +#include +#include +#include + +#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__) \ No newline at end of file From 587e7620f10da43905ea0433c5f2bdbd204de05e Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 17 Aug 2025 16:59:15 +0200 Subject: [PATCH 2/6] Update doc --- .gitignore | 1 + README.md | 14 ----- doc/posix_pipe.md | 55 +++++++++++++++++++ .../ftxui/component/screen_interactive.hpp | 2 +- src/ftxui/component/screen_interactive.cpp | 4 +- .../screen_interactive_piped_input_test.cpp | 23 ++++---- 6 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 doc/posix_pipe.md diff --git a/.gitignore b/.gitignore index 40a2b7c49..70d197c72 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ out/ !doc/**/*.html !doc/**/*.xml !doc/**/*.md +!doc/*.md # examples directory: !examples/**/*.cpp diff --git a/README.md b/README.md index fdfdd28ca..256840557 100644 --- a/README.md +++ b/README.md @@ -378,21 +378,7 @@ 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 diff --git a/doc/posix_pipe.md b/doc/posix_pipe.md new file mode 100644 index 000000000..b5ad8f0f6 --- /dev/null +++ b/doc/posix_pipe.md @@ -0,0 +1,55 @@ +# POSIX Piped Input in FTXUI + +## 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); +``` + +**Note:** This feature works only on Linux and macOS. It does nothing on Windows. + +## 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); +``` \ No newline at end of file diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 0291e76e3..e723caff5 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -144,7 +144,7 @@ class ScreenInteractive : public Screen { #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) // Piped input handling state (POSIX only) - bool handle_piped_input_ = false; + bool handle_piped_input_ = true; bool stdin_was_redirected_ = false; int original_stdin_fd_ = -1; #endif diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index fad541130..8836b6e43 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -376,9 +376,9 @@ void ScreenInteractive::TrackMouse(bool enable) { /// 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 +/// @param enable Whether to enable piped input handling. Default is true. /// @note This must be called before Loop(). -/// @note This feature is disabled by default for backward compatibility. +/// @note This feature is enabled by default. /// @note This feature is only available on POSIX systems (Linux/macOS). #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) void ScreenInteractive::HandlePipedInput(bool enable) { diff --git a/src/ftxui/component/screen_interactive_piped_input_test.cpp b/src/ftxui/component/screen_interactive_piped_input_test.cpp index e953bbd30..643f8bb8d 100644 --- a/src/ftxui/component/screen_interactive_piped_input_test.cpp +++ b/src/ftxui/component/screen_interactive_piped_input_test.cpp @@ -63,20 +63,24 @@ class PipedInputTest : public ::testing::Test { bool piped_stdin_setup_ = false; }; -TEST_F(PipedInputTest, DefaultBehaviorNoChange) { - // Test that HandlePipedInput is disabled by default +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 not redirect stdin since HandlePipedInput not called + // Install should redirect stdin since HandlePipedInput is on by default screen.Install(); - - // Stdin should still be the pipe (isatty should return false) - EXPECT_FALSE(isatty(STDIN_FILENO)); - + + // Stdin should be the tty + EXPECT_TRUE(isatty(STDIN_FILENO)); + screen.Uninstall(); } @@ -97,7 +101,7 @@ TEST_F(PipedInputTest, ExplicitlyDisabled) { screen.Uninstall(); } -TEST_F(PipedInputTest, PipedInputDetectionAndRedirection) { +TEST_F(PipedInputTest, ExplicitlyEnabled) { if (!IsTtyAvailable()) { GTEST_SKIP() << "/dev/tty not available in this environment"; } @@ -127,7 +131,6 @@ TEST_F(PipedInputTest, PipedInputDetectionAndRedirection) { 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 @@ -150,7 +153,6 @@ TEST_F(PipedInputTest, MultipleInstallUninstallCycles) { } auto screen = ScreenInteractive::TerminalOutput(); - screen.HandlePipedInput(true); auto component = Renderer([] { return text("test"); }); SetupPipedStdin(); @@ -192,7 +194,6 @@ TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) { // 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(); From be39aed59205a31f97f086d237ef3db54b336892 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 17 Aug 2025 17:24:08 +0200 Subject: [PATCH 3/6] Tweak implementation and documentation. --- doc/posix_pipe.md | 7 +- .../ftxui/component/screen_interactive.hpp | 9 +- src/ftxui/component/screen_interactive.cpp | 83 ++++++++++--------- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/doc/posix_pipe.md b/doc/posix_pipe.md index b5ad8f0f6..ebbe7ce10 100644 --- a/doc/posix_pipe.md +++ b/doc/posix_pipe.md @@ -1,5 +1,9 @@ # 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. @@ -40,7 +44,6 @@ auto screen = ScreenInteractive::Fullscreen(); screen.Loop(component); ``` -**Note:** This feature works only on Linux and macOS. It does nothing on Windows. ## Turning Off Piped Input @@ -52,4 +55,4 @@ To disable it, call `HandlePipedInput(false)` before starting your application's auto screen = ScreenInteractive::Fullscreen(); screen.HandlePipedInput(false); // Turn off piped input handling screen.Loop(component); -``` \ No newline at end of file +``` diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index e723caff5..b3d1f5156 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -43,7 +43,7 @@ class ScreenInteractive : public Screen { static ScreenInteractive TerminalOutput(); // Destructor. - ~ScreenInteractive(); + ~ScreenInteractive() override; // Options. Must be called before Loop(). void TrackMouse(bool enable = true); @@ -101,6 +101,8 @@ class ScreenInteractive : public Screen { void Draw(Component component); void ResetCursorPosition(); + void InstallPipedInputHandling(); + void Signal(int signal); void FetchTerminalEvents(); @@ -118,6 +120,7 @@ class ScreenInteractive : public Screen { int dimx, int dimy, bool use_alternative_screen); + const Dimension dimension_; const bool use_alternative_screen_; @@ -142,12 +145,8 @@ 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_ = true; - 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; diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 8836b6e43..41b7028a3 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -373,22 +373,16 @@ void ScreenInteractive::TrackMouse(bool enable) { } /// @brief Enable or disable automatic piped input handling. -/// When enabled, FTXUI will detect piped input and redirect stdin to /dev/tty +/// 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). -#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. @@ -676,47 +670,58 @@ 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 + // 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(); - -#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(); } From b0ba518d85506f9ffed4a1537231458b9557515b Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 17 Aug 2025 19:32:28 +0200 Subject: [PATCH 4/6] docs: Update CHANGELOG.md for POSIX Piped Input Handling Added an entry for the POSIX Piped Input Handling feature, including details about its functionality, default state, and how to disable it. Also attributed @HarryPehkonen for their contribution in PR #1094. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1c62b27..857bb2e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ Next - Remove dependency on 'pthread'. ### Component +- Feature: POSIX Piped Input Handling. + - Allows FTXUI applications to read data from stdin (when piped) while still receiving keyboard input from the terminal. + - Enabled by default. + - Can be disabled using `ScreenInteractive::HandlePipedInput(false)`. + - Only available on Linux and macOS. + Thanks @HarryPehkonen for PR #1094. - Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal output. Thanks @zozowell in #1064. From 4b0d0984a5076cded283e21591594ec18562f0d2 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 17 Aug 2025 19:40:20 +0200 Subject: [PATCH 5/6] feat: Improve POSIX piped input handling Refactored the POSIX piped input handling to avoid redirecting stdin. Instead, it now directly opens and reads from /dev/tty for keyboard input when stdin is piped, allowing applications to process piped data while still receiving interactive keyboard events. This addresses the TODO comment in ScreenInteractive::InstallPipedInputHandling(). --- .../ftxui/component/screen_interactive.hpp | 2 + src/ftxui/component/screen_interactive.cpp | 37 ++++++++----------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index b3d1f5156..aff53bc31 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -147,6 +147,8 @@ class ScreenInteractive : public Screen { // Piped input handling state (POSIX only) bool handle_piped_input_ = true; + // File descriptor for /dev/tty, used for piped input handling. + int tty_fd_ = -1; // The style of the cursor to restore on exit. int cursor_reset_shape_ = 1; diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 41b7028a3..3bca110d4 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -112,13 +112,13 @@ void ftxui_on_resize(int columns, int rows) { #else // POSIX (Linux & Mac) -int CheckStdinReady() { +int CheckStdinReady(int fd) { timeval tv = {0, 0}; // NOLINT fd_set fds; FD_ZERO(&fds); // NOLINT - FD_SET(STDIN_FILENO, &fds); // NOLINT - select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT - return FD_ISSET(STDIN_FILENO, &fds); // NOLINT + FD_SET(fd, &fds); // NOLINT + select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT + return FD_ISSET(fd, &fds); // NOLINT } #endif @@ -684,6 +684,7 @@ void ScreenInteractive::Install() { } void ScreenInteractive::InstallPipedInputHandling() { + tty_fd_ = STDIN_FILENO; // Default to stdin. #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 @@ -692,29 +693,23 @@ void ScreenInteractive::InstallPipedInputHandling() { return; } - // If stdin is a terminal, we don't need to redirect it. + // If stdin is a terminal, we don't need to open /dev/tty. 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) { + // Open /dev/tty for keyboard input. + tty_fd_ = open("/dev/tty", O_RDONLY); + if (tty_fd_ < 0) { // Failed to open /dev/tty (containers, headless systems, etc.) - // Clean up and continue without redirection - close(original_fd); + tty_fd_ = STDIN_FILENO; // Fallback to stdin. return; } - // Restore the original stdin file descriptor on exit. - on_exit_functions.emplace([=] { - dup2(original_fd, STDIN_FILENO); - close(original_fd); + // Close the /dev/tty file descriptor on exit. + on_exit_functions.emplace([this] { + close(tty_fd_); + tty_fd_ = -1; }); #endif } @@ -1152,7 +1147,7 @@ void ScreenInteractive::FetchTerminalEvents() { internal_->terminal_input_parser.Add(out[i]); } #else // POSIX (Linux & Mac) - if (!CheckStdinReady()) { + if (!CheckStdinReady(tty_fd_)) { const auto timeout = std::chrono::steady_clock::now() - internal_->last_char_time; const size_t timeout_ms = @@ -1164,7 +1159,7 @@ void ScreenInteractive::FetchTerminalEvents() { // Read chars from the terminal. std::array out{}; - size_t l = read(fileno(stdin), out.data(), out.size()); + size_t l = read(tty_fd_, out.data(), out.size()); // Convert the chars to events. for (size_t i = 0; i < l; ++i) { From e85d655b4db520e17ce83161e80e95ca6cd2cb98 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 17 Aug 2025 20:21:20 +0200 Subject: [PATCH 6/6] Fix stdin references. --- src/ftxui/component/screen_interactive.cpp | 25 +++++++++------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 3bca110d4..ea443c5cc 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -539,6 +539,8 @@ void ScreenInteractive::Install() { // https://github.com/ArthurSonzogni/FTXUI/issues/846 Flush(); + InstallPipedInputHandling(); + // After uninstalling the new configuration, flush it to the terminal to // ensure it is fully applied: on_exit_functions.emplace([] { Flush(); }); @@ -604,9 +606,10 @@ void ScreenInteractive::Install() { } struct termios terminal; // NOLINT - tcgetattr(STDIN_FILENO, &terminal); - on_exit_functions.emplace( - [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); }); + tcgetattr(tty_fd_, &terminal); + on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] { + tcsetattr(tty_fd_, TCSANOW, &terminal); + }); // Enabling raw terminal input mode terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition @@ -634,7 +637,7 @@ void ScreenInteractive::Install() { // read. terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read. - tcsetattr(STDIN_FILENO, TCSANOW, &terminal); + tcsetattr(tty_fd_, TCSANOW, &terminal); #endif @@ -670,21 +673,13 @@ 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() { - tty_fd_ = STDIN_FILENO; // Default to stdin. + tty_fd_ = fileno(stdin); // NOLINT #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 @@ -694,7 +689,7 @@ void ScreenInteractive::InstallPipedInputHandling() { } // If stdin is a terminal, we don't need to open /dev/tty. - if (isatty(STDIN_FILENO)) { + if (isatty(fileno(stdin))) { return; } @@ -702,7 +697,7 @@ void ScreenInteractive::InstallPipedInputHandling() { tty_fd_ = open("/dev/tty", O_RDONLY); if (tty_fd_ < 0) { // Failed to open /dev/tty (containers, headless systems, etc.) - tty_fd_ = STDIN_FILENO; // Fallback to stdin. + tty_fd_ = fileno(stdin); // Fallback to stdin. return; }