6 Commits

Author SHA1 Message Date
ArthurSonzogni
e85d655b4d Fix stdin references. 2025-08-17 20:21:20 +02:00
ArthurSonzogni
4b0d0984a5 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().
2025-08-17 19:48:54 +02:00
ArthurSonzogni
b0ba518d85 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.
2025-08-17 19:32:28 +02:00
ArthurSonzogni
be39aed592 Tweak implementation and documentation. 2025-08-17 19:24:26 +02:00
ArthurSonzogni
587e7620f1 Update doc 2025-08-17 19:24:26 +02:00
Harri Pehkonen
def5c6d50c 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.
2025-08-17 19:24:26 +02:00
11 changed files with 110 additions and 227 deletions

View File

@@ -35,8 +35,6 @@ Next
Thanks @HarryPehkonen for PR #1094. Thanks @HarryPehkonen for PR #1094.
- Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal - Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal
output. Thanks @zozowell in #1064. output. Thanks @zozowell in #1064.
- Fix vertical `ftxui::Slider`. The "up" key was previously decreasing the
value. Thanks @its-pablo in #1093 for reporting the issue.
6.1.9 (2025-05-07) 6.1.9 (2025-05-07)

View File

@@ -178,8 +178,8 @@ include(cmake/ftxui_install.cmake)
include(cmake/ftxui_package.cmake) include(cmake/ftxui_package.cmake)
include(cmake/ftxui_modules.cmake) include(cmake/ftxui_modules.cmake)
add_subdirectory(examples)
add_subdirectory(doc) add_subdirectory(doc)
add_subdirectory(examples)
# You can generate ./examples_modules/ by running # You can generate ./examples_modules/ by running
# ./tools/generate_examples_modules.sh # ./tools/generate_examples_modules.sh

View File

@@ -17,12 +17,10 @@ add_subdirectory(dom)
if (EMSCRIPTEN) if (EMSCRIPTEN)
get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES) get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES)
foreach(file foreach(file
"index.css"
"index.html" "index.html"
"index.mjs" "index.mjs"
"run_webassembly.py" "index.css"
"sw.js" "run_webassembly.py")
)
configure_file(${file} ${file}) configure_file(${file} ${file})
endforeach(file) endforeach(file)
endif() endif()

View File

@@ -1,19 +1,15 @@
@import url(https://fonts.googleapis.com/css?family=Khula:700); @import url(https://fonts.googleapis.com/css?family=Khula:700);
html {
--toc-width: 250px;
}
body { body {
background-color: #EEE; background-color:#EEE;
padding: 0px; padding:0px;
margin: 0px; margin:0px;
font-family: Khula, Helvetica, sans-serif; font-family: Khula, Helvetica, sans-serif;
font-size: 130%; font-size: 130%;
} }
.page { .page {
max-width: 1300px; max-width:1300px;
margin: auto; margin: auto;
padding: 10px; padding: 10px;
} }
@@ -24,7 +20,7 @@ a {
margin: 0 -.25rem; margin: 0 -.25rem;
padding: 0 .25rem; padding: 0 .25rem;
transition: color .3s ease-in-out, transition: color .3s ease-in-out,
box-shadow .3s ease-in-out; box-shadow .3s ease-in-out;
} }
a:hover { a:hover {
@@ -34,48 +30,45 @@ a:hover {
h1 { h1 {
text-decoration: underline; text-decoration: underline;
width: 100%; width:100%;
background-color: rgba(100, 100, 255, 0.5); background-color: rgba(100,100,255,0.5);
padding: 10px; padding: 10px;
margin: 0; margin: 0;
} }
#selectExample { #selectExample {
flex: 1; flex:1;
} }
#selectExample, #selectExample, #selectExample option {
#selectExample option {
font-size: 16px; font-size: 16px;
font-family: sans-serif; font-family: sans-serif;
font-weight: 700; font-weight: 700;
line-height: 1.3; line-height: 1.3;
border: 0px; border:0px;
background-color: #bbb; background-color: #bbb;
color: black; color:black;
} }
#selectExample:focus { #selectExample:focus {
outline: none; outline:none;
} }
#terminal { #terminal {
width: 100%; width:100%;
height 500px; height 500px;
height: calc(clamp(200px, 100vh - 300px, 900px)); height: calc(clamp(200px, 100vh - 300px, 900px));
overflow: hidden; overflow: hidden;
border: none; border:none;
padding: 10px; background-color:black;
margin: 10px;
} }
#terminalContainer { #terminalContainer {
overflow: hidden; overflow: hidden;
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.75), box-shadow: 0px 2px 10px 0px rgba(0,0,0,0.75),
0px 2px 80px 0px rgba(0, 0, 0, 0.50); 0px 2px 80px 0px rgba(0,0,0,0.50);
background-color: black;
} }
.fakeButtons { .fakeButtons {
@@ -83,7 +76,7 @@ h1 {
width: 10px; width: 10px;
border-radius: 50%; border-radius: 50%;
border: 1px solid #000; border: 1px solid #000;
margin: 6px; margin:6px;
background-color: #ff3b47; background-color: #ff3b47;
border-color: #9d252b; border-color: #9d252b;
display: inline-block; display: inline-block;
@@ -102,79 +95,13 @@ h1 {
} }
.fakeMenu { .fakeMenu {
display: flex; display:flex;
flex-direction: row; flex-direction: row;
width: 100%; width:100%;
box-sizing: border-box; box-sizing: border-box;
height: 25px; height: 25px;
background-color: #bbb; background-color: #bbb;
color: black; color:black;
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
} }
.toc-container {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--toc-width);
background: white;
padding: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.toc-title {
font-weight: bold;
margin-bottom: 5px;
font-size: 0.9em;
color: #555;
position: sticky;
transition: position 1.0s ease-in-out;
top: 0;
z-index: 1;
padding: 20px;
margin: 0;
border-bottom: 1px solid #ddd;
/* Gradient background for the title */
background-color: #f0f0f0;
}
.toc-item {
padding: 3px 8px;
margin: 0;
cursor: pointer;
font-size: 0.85em;
border-radius: 3px;
transition: background 0.2s;
}
.toc-item:hover {
background: #f0f0f0;
}
.toc-item.selected {
background: #e0e0e0;
font-weight: bold;
}
@media (max-width: 1024px) {
.toc-container {
display: none;
}
.page {
margin-left: 0;
}
}
@media (min-width: 1025px) {
.page {
margin-left: calc(var(--toc-width) + 20px);
}
}

View File

@@ -9,18 +9,13 @@
<script type="module" src="index.mjs"></script> <script type="module" src="index.mjs"></script>
</head> </head>
<body> <body>
<div class="toc-container">
<div class="toc-list"></div>
</div>
<script id="example_script"></script> <script id="example_script"></script>
<div class="page"> <div class="page">
<p> <p>
<a href="https://github.com/ArthurSonzogni/FTXUI">FTXUI</a> is a simple <a href="https://github.com/ArthurSonzogni/FTXUI">FTXUI</a> is a simple
functional C++ library for terminal user interface. <br/> functional C++ library for terminal user interface. <br/>
This showcases the: <a This showcases the: <a href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a> folder. <br/>
href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a>
folder. See <a id="source">source</a>.
</p> </p>
<div id="terminalContainer"> <div id="terminalContainer">

View File

@@ -92,69 +92,6 @@ window.Module = {
}, },
}; };
const source = document.querySelector("#source");
source.href = "https://github.com/ArthurSonzogni/FTXUI/blob/main/examples/" + example + ".cpp";
const words = example.split('/') const words = example.split('/')
words[1] = "ftxui_example_" + words[1] + ".js" words[1] = "ftxui_example_" + words[1] + ".js"
document.querySelector("#example_script").src = words.join('/'); document.querySelector("#example_script").src = words.join('/');
// Table of Contents (TOC) for quick navigation.
// Get select element
const selectEl = document.querySelector('select#selectExample');
if (!selectEl) {
console.error('select#selectExample not found');
} else {
// Get TOC container
const tocContainer = document.querySelector('.toc-container');
const tocList = tocContainer.querySelector('.toc-list');
// Group options by directory
const groupedOptions = Array.from(selectEl.options).reduce((acc, option) => {
const [dir, file] = option.text.split('/');
if (!acc[dir]) {
acc[dir] = [];
}
acc[dir].push({ option, file });
return acc;
}, {});
// Generate TOC items
for (const dir in groupedOptions) {
const dirContainer = document.createElement('div');
const dirHeader = document.createElement('div');
dirHeader.textContent = dir;
dirHeader.className = 'toc-title';
dirContainer.appendChild(dirHeader);
groupedOptions[dir].forEach(({ option, file }) => {
const tocItem = document.createElement('div');
tocItem.textContent = file;
tocItem.className = 'toc-item';
if (selectEl.options[selectEl.selectedIndex].value === option.value) {
tocItem.classList.add('selected');
}
// Click handler
tocItem.addEventListener('click', () => {
for(let i=0; i<selectEl.options.length; ++i) {
if (selectEl.options[i].value == option.value) {
selectEl.selectedIndex = i;
break;
}
}
history.pushState({}, "", "?file=" + option.value);
location.reload();
});
dirContainer.appendChild(tocItem);
});
tocList.appendChild(dirContainer);
}
}''

View File

@@ -4,6 +4,7 @@
#ifndef FTXUI_COMPONENT_RECEIVER_HPP_ #ifndef FTXUI_COMPONENT_RECEIVER_HPP_
#define FTXUI_COMPONENT_RECEIVER_HPP_ #define FTXUI_COMPONENT_RECEIVER_HPP_
#include <ftxui/util/warn_windows_macro.h>
#include <algorithm> // for copy, max #include <algorithm> // for copy, max
#include <atomic> // for atomic, __atomic_base #include <atomic> // for atomic, __atomic_base
#include <condition_variable> // for condition_variable #include <condition_variable> // for condition_variable
@@ -11,7 +12,6 @@
#include <mutex> // for mutex, unique_lock #include <mutex> // for mutex, unique_lock
#include <queue> // for queue #include <queue> // for queue
#include <utility> // for move #include <utility> // for move
#include "ftxui/util/warn_windows_macro.hpp"
namespace ftxui { namespace ftxui {

View File

@@ -2,8 +2,8 @@
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_HPP_ #ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#define FTXUI_UTIL_WARN_WINDOWS_MACRO_HPP_ #define FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#ifdef min #ifdef min
#error \ #error \
@@ -15,4 +15,4 @@
"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." "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
#endif // FTXUI_UTIL_WARN_WINDOWS_MACRO_HPP_ #endif // FTXUI_UTIL_WARN_WINDOWS_MACRO_H_

View File

@@ -115,7 +115,7 @@ void ftxui_on_resize(int columns, int rows) {
int CheckStdinReady(int fd) { int CheckStdinReady(int fd) {
timeval tv = {0, 0}; // NOLINT timeval tv = {0, 0}; // NOLINT
fd_set fds; fd_set fds;
FD_ZERO(&fds); // NOLINT FD_ZERO(&fds); // NOLINT
FD_SET(fd, &fds); // NOLINT FD_SET(fd, &fds); // NOLINT
select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
return FD_ISSET(fd, &fds); // NOLINT return FD_ISSET(fd, &fds); // NOLINT
@@ -679,7 +679,7 @@ void ScreenInteractive::Install() {
} }
void ScreenInteractive::InstallPipedInputHandling() { void ScreenInteractive::InstallPipedInputHandling() {
tty_fd_ = STDIN_FILENO; tty_fd_ = fileno(stdin); // NOLINT
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) #if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
// Handle piped input redirection if explicitly enabled by the application. // Handle piped input redirection if explicitly enabled by the application.
// This allows applications to read data from stdin while still receiving // This allows applications to read data from stdin while still receiving
@@ -689,7 +689,7 @@ void ScreenInteractive::InstallPipedInputHandling() {
} }
// If stdin is a terminal, we don't need to open /dev/tty. // If stdin is a terminal, we don't need to open /dev/tty.
if (isatty(STDIN_FILENO)) { if (isatty(fileno(stdin))) {
return; return;
} }
@@ -697,7 +697,7 @@ void ScreenInteractive::InstallPipedInputHandling() {
tty_fd_ = open("/dev/tty", O_RDONLY); tty_fd_ = open("/dev/tty", O_RDONLY);
if (tty_fd_ < 0) { if (tty_fd_ < 0) {
// Failed to open /dev/tty (containers, headless systems, etc.) // Failed to open /dev/tty (containers, headless systems, etc.)
tty_fd_ = STDIN_FILENO; // Fallback to stdin. tty_fd_ = fileno(stdin); // Fallback to stdin.
return; return;
} }

View File

@@ -1,11 +1,11 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved. // Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#include <fcntl.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h>
#include <cstdio> #include <cstdio>
#include <sys/stat.h>
#include "ftxui/component/component.hpp" #include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp" #include "ftxui/component/screen_interactive.hpp"
@@ -47,7 +47,7 @@ class PipedInputTest : public ::testing::Test {
void WriteToPipedStdin(const std::string& data) { void WriteToPipedStdin(const std::string& data) {
if (piped_stdin_setup_) { if (piped_stdin_setup_) {
write(pipe_fds_[1], data.c_str(), data.length()); write(pipe_fds_[1], data.c_str(), data.length());
close(pipe_fds_[1]); // Close write end to signal EOF close(pipe_fds_[1]); // Close write end to signal EOF
} }
} }
@@ -94,10 +94,10 @@ TEST_F(PipedInputTest, ExplicitlyDisabled) {
WriteToPipedStdin("test data\n"); WriteToPipedStdin("test data\n");
screen.Install(); screen.Install();
// Stdin should still be the pipe since feature is disabled // Stdin should still be the pipe since feature is disabled
EXPECT_FALSE(isatty(STDIN_FILENO)); EXPECT_FALSE(isatty(STDIN_FILENO));
screen.Uninstall(); screen.Uninstall();
} }
@@ -107,7 +107,7 @@ TEST_F(PipedInputTest, ExplicitlyEnabled) {
} }
auto screen = ScreenInteractive::TerminalOutput(); auto screen = ScreenInteractive::TerminalOutput();
screen.HandlePipedInput(true); // Explicitly enable screen.HandlePipedInput(true); // Explicitly enable
auto component = Renderer([] { return text("test"); }); auto component = Renderer([] { return text("test"); });
SetupPipedStdin(); SetupPipedStdin();
@@ -117,12 +117,12 @@ TEST_F(PipedInputTest, ExplicitlyEnabled) {
EXPECT_FALSE(isatty(STDIN_FILENO)); EXPECT_FALSE(isatty(STDIN_FILENO));
screen.Install(); screen.Install();
// After install with piped input handling: stdin should be redirected to tty // After install with piped input handling: stdin should be redirected to tty
EXPECT_TRUE(isatty(STDIN_FILENO)); EXPECT_TRUE(isatty(STDIN_FILENO));
screen.Uninstall(); screen.Uninstall();
// After uninstall: stdin should be restored to original state // After uninstall: stdin should be restored to original state
// Note: This will be the pipe we set up, so it should be non-tty // Note: This will be the pipe we set up, so it should be non-tty
EXPECT_FALSE(isatty(STDIN_FILENO)); EXPECT_FALSE(isatty(STDIN_FILENO));
@@ -137,12 +137,12 @@ TEST_F(PipedInputTest, NormalStdinUnchanged) {
bool original_isatty = isatty(STDIN_FILENO); bool original_isatty = isatty(STDIN_FILENO);
screen.Install(); screen.Install();
// Stdin should remain unchanged // Stdin should remain unchanged
EXPECT_EQ(original_isatty, isatty(STDIN_FILENO)); EXPECT_EQ(original_isatty, isatty(STDIN_FILENO));
screen.Uninstall(); screen.Uninstall();
// Stdin should still be unchanged // Stdin should still be unchanged
EXPECT_EQ(original_isatty, isatty(STDIN_FILENO)); EXPECT_EQ(original_isatty, isatty(STDIN_FILENO));
} }
@@ -173,12 +173,12 @@ TEST_F(PipedInputTest, MultipleInstallUninstallCycles) {
TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) { TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) {
auto screen = ScreenInteractive::TerminalOutput(); auto screen = ScreenInteractive::TerminalOutput();
// Test method can be called multiple times // Test method can be called multiple times
screen.HandlePipedInput(true); screen.HandlePipedInput(true);
screen.HandlePipedInput(false); screen.HandlePipedInput(false);
screen.HandlePipedInput(true); screen.HandlePipedInput(true);
// Should be enabled after last call // Should be enabled after last call
SetupPipedStdin(); SetupPipedStdin();
WriteToPipedStdin("test data\n"); WriteToPipedStdin("test data\n");
@@ -191,8 +191,7 @@ TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) {
} }
// Test the graceful fallback when /dev/tty is not available // Test the graceful fallback when /dev/tty is not available
// This test simulates environments like containers where /dev/tty might not // This test simulates environments like containers where /dev/tty might not exist
// exist
TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) { TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) {
auto screen = ScreenInteractive::TerminalOutput(); auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); }); auto component = Renderer([] { return text("test"); });
@@ -200,23 +199,22 @@ TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) {
SetupPipedStdin(); SetupPipedStdin();
WriteToPipedStdin("test data\n"); WriteToPipedStdin("test data\n");
// This test doesn't directly mock /dev/tty unavailability since that's hard // This test doesn't directly mock /dev/tty unavailability since that's hard to do
// to do in a unit test environment, but the code path handles freopen() // in a unit test environment, but the code path handles freopen() failure gracefully
// failure gracefully
screen.Install(); screen.Install();
// The behavior depends on whether /dev/tty is available // The behavior depends on whether /dev/tty is available
// If available, stdin gets redirected; if not, it remains piped // If available, stdin gets redirected; if not, it remains piped
// Both behaviors are correct // Both behaviors are correct
screen.Uninstall(); screen.Uninstall();
// After uninstall, stdin should be restored // After uninstall, stdin should be restored
EXPECT_FALSE(isatty(STDIN_FILENO)); // Should still be our test pipe EXPECT_FALSE(isatty(STDIN_FILENO)); // Should still be our test pipe
} }
} // namespace } // namespace
} // namespace ftxui } // namespace ftxui
#endif // !defined(_WIN32) && !defined(__EMSCRIPTEN__) #endif // !defined(_WIN32) && !defined(__EMSCRIPTEN__)

View File

@@ -33,20 +33,6 @@ Decorator flexDirection(Direction direction) {
return xflex; // NOT_REACHED() return xflex; // NOT_REACHED()
} }
Direction Opposite(Direction d) {
switch (d) {
case Direction::Up:
return Direction::Down;
case Direction::Down:
return Direction::Up;
case Direction::Left:
return Direction::Right;
case Direction::Right:
return Direction::Left;
}
return d; // NOT_REACHED()
}
template <class T> template <class T>
class SliderBase : public SliderOption<T>, public ComponentBase { class SliderBase : public SliderOption<T>, public ComponentBase {
public: public:
@@ -61,15 +47,59 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
flexDirection(this->direction) | reflect(gauge_box_) | gauge_color; flexDirection(this->direction) | reflect(gauge_box_) | gauge_color;
} }
void OnDirection(Direction pressed) { void OnLeft() {
if (pressed == this->direction) { switch (this->direction) {
this->value() += this->increment(); case Direction::Right:
return; this->value() -= this->increment();
break;
case Direction::Left:
this->value() += this->increment();
break;
case Direction::Up:
case Direction::Down:
break;
} }
}
if (pressed == Opposite(this->direction)) { void OnRight() {
this->value() -= this->increment(); switch (this->direction) {
return; case Direction::Right:
this->value() += this->increment();
break;
case Direction::Left:
this->value() -= this->increment();
break;
case Direction::Up:
case Direction::Down:
break;
}
}
void OnUp() {
switch (this->direction) {
case Direction::Up:
this->value() -= this->increment();
break;
case Direction::Down:
this->value() += this->increment();
break;
case Direction::Left:
case Direction::Right:
break;
}
}
void OnDown() {
switch (this->direction) {
case Direction::Down:
this->value() += this->increment();
break;
case Direction::Up:
this->value() -= this->increment();
break;
case Direction::Left:
case Direction::Right:
break;
} }
} }
@@ -80,16 +110,16 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
T old_value = this->value(); T old_value = this->value();
if (event == Event::ArrowLeft || event == Event::Character('h')) { if (event == Event::ArrowLeft || event == Event::Character('h')) {
OnDirection(Direction::Left); OnLeft();
} }
if (event == Event::ArrowRight || event == Event::Character('l')) { if (event == Event::ArrowRight || event == Event::Character('l')) {
OnDirection(Direction::Right); OnRight();
} }
if (event == Event::ArrowUp || event == Event::Character('k')) { if (event == Event::ArrowUp || event == Event::Character('k')) {
OnDirection(Direction::Up); OnDown();
} }
if (event == Event::ArrowDown || event == Event::Character('j')) { if (event == Event::ArrowDown || event == Event::Character('j')) {
OnDirection(Direction::Down); OnUp();
} }
this->value() = std::max(this->min(), std::min(this->max(), this->value())); this->value() = std::max(this->min(), std::min(this->max(), this->value()));