mirror of
				https://github.com/ArthurSonzogni/FTXUI.git
				synced 2025-10-31 18:48:11 +08:00 
			
		
		
		
	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
					Harri Pehkonen
				
			
				
					committed by
					
						 ArthurSonzogni
						ArthurSonzogni
					
				
			
			
				
	
			
			
			 ArthurSonzogni
						ArthurSonzogni
					
				
			
						parent
						
							40e1fac3d4
						
					
				
				
					commit
					143b24c6a5
				
			
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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. | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										219
									
								
								src/ftxui/component/screen_interactive_piped_input_test.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/ftxui/component/screen_interactive_piped_input_test.cpp
									
									
									
									
									
										Normal 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__) | ||||
		Reference in New Issue
	
	Block a user