mirror of
				https://github.com/ArthurSonzogni/FTXUI.git
				synced 2025-11-01 02:58:12 +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) | - [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) | - [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 | ## 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. | 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(). |   // Options. Must be called before Loop(). | ||||||
|   void TrackMouse(bool enable = true); |   void TrackMouse(bool enable = true); | ||||||
|  |   void HandlePipedInput(bool enable = true); | ||||||
|  |  | ||||||
|   // Return the currently active screen, nullptr if none. |   // Return the currently active screen, nullptr if none. | ||||||
|   static ScreenInteractive* Active(); |   static ScreenInteractive* Active(); | ||||||
| @@ -141,6 +142,13 @@ class ScreenInteractive : public Screen { | |||||||
|   bool force_handle_ctrl_c_ = true; |   bool force_handle_ctrl_c_ = true; | ||||||
|   bool force_handle_ctrl_z_ = 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. |   // The style of the cursor to restore on exit. | ||||||
|   int cursor_reset_shape_ = 1; |   int cursor_reset_shape_ = 1; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -372,6 +372,24 @@ void ScreenInteractive::TrackMouse(bool enable) { | |||||||
|   track_mouse_ = 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. | /// @brief Add a task to the main loop. | ||||||
| /// It will be executed later, after every other scheduled tasks. | /// It will be executed later, after every other scheduled tasks. | ||||||
| void ScreenInteractive::Post(Task task) { | void ScreenInteractive::Post(Task task) { | ||||||
| @@ -658,6 +676,28 @@ void ScreenInteractive::Install() { | |||||||
|   // ensure it is fully applied: |   // ensure it is fully applied: | ||||||
|   Flush(); |   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; |   quit_ = false; | ||||||
|  |  | ||||||
|   PostAnimationTask(); |   PostAnimationTask(); | ||||||
| @@ -666,6 +706,17 @@ void ScreenInteractive::Install() { | |||||||
| // private | // private | ||||||
| void ScreenInteractive::Uninstall() { | void ScreenInteractive::Uninstall() { | ||||||
|   ExitNow(); |   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(); |   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