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.
- Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal
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)

View File

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

View File

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

View File

@@ -1,19 +1,15 @@
@import url(https://fonts.googleapis.com/css?family=Khula:700);
html {
--toc-width: 250px;
}
body {
background-color: #EEE;
padding: 0px;
margin: 0px;
background-color:#EEE;
padding:0px;
margin:0px;
font-family: Khula, Helvetica, sans-serif;
font-size: 130%;
}
.page {
max-width: 1300px;
max-width:1300px;
margin: auto;
padding: 10px;
}
@@ -24,7 +20,7 @@ a {
margin: 0 -.25rem;
padding: 0 .25rem;
transition: color .3s ease-in-out,
box-shadow .3s ease-in-out;
box-shadow .3s ease-in-out;
}
a:hover {
@@ -34,48 +30,45 @@ a:hover {
h1 {
text-decoration: underline;
width: 100%;
background-color: rgba(100, 100, 255, 0.5);
width:100%;
background-color: rgba(100,100,255,0.5);
padding: 10px;
margin: 0;
}
#selectExample {
flex: 1;
flex:1;
}
#selectExample,
#selectExample option {
#selectExample, #selectExample option {
font-size: 16px;
font-family: sans-serif;
font-weight: 700;
line-height: 1.3;
border: 0px;
border:0px;
background-color: #bbb;
color: black;
color:black;
}
#selectExample:focus {
outline: none;
outline:none;
}
#terminal {
width: 100%;
width:100%;
height 500px;
height: calc(clamp(200px, 100vh - 300px, 900px));
overflow: hidden;
border: none;
padding: 10px;
margin: 10px;
border:none;
background-color:black;
}
#terminalContainer {
overflow: hidden;
border-radius: 10px;
box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.75),
0px 2px 80px 0px rgba(0, 0, 0, 0.50);
background-color: black;
box-shadow: 0px 2px 10px 0px rgba(0,0,0,0.75),
0px 2px 80px 0px rgba(0,0,0,0.50);
}
.fakeButtons {
@@ -83,7 +76,7 @@ h1 {
width: 10px;
border-radius: 50%;
border: 1px solid #000;
margin: 6px;
margin:6px;
background-color: #ff3b47;
border-color: #9d252b;
display: inline-block;
@@ -102,79 +95,13 @@ h1 {
}
.fakeMenu {
display: flex;
display:flex;
flex-direction: row;
width: 100%;
width:100%;
box-sizing: border-box;
height: 25px;
background-color: #bbb;
color: black;
color:black;
margin: 0 auto;
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>
</head>
<body>
<div class="toc-container">
<div class="toc-list"></div>
</div>
<script id="example_script"></script>
<div class="page">
<p>
<a href="https://github.com/ArthurSonzogni/FTXUI">FTXUI</a> is a simple
functional C++ library for terminal user interface. <br/>
This showcases the: <a
href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a>
folder. See <a id="source">source</a>.
This showcases the: <a href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a> folder. <br/>
</p>
<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('/')
words[1] = "ftxui_example_" + words[1] + ".js"
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_
#define FTXUI_COMPONENT_RECEIVER_HPP_
#include <ftxui/util/warn_windows_macro.h>
#include <algorithm> // for copy, max
#include <atomic> // for atomic, __atomic_base
#include <condition_variable> // for condition_variable
@@ -11,7 +12,6 @@
#include <mutex> // for mutex, unique_lock
#include <queue> // for queue
#include <utility> // for move
#include "ftxui/util/warn_windows_macro.hpp"
namespace ftxui {

View File

@@ -2,8 +2,8 @@
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_HPP_
#define FTXUI_UTIL_WARN_WINDOWS_MACRO_HPP_
#ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#define FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#ifdef min
#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."
#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) {
timeval tv = {0, 0}; // NOLINT
fd_set fds;
FD_ZERO(&fds); // NOLINT
FD_ZERO(&fds); // NOLINT
FD_SET(fd, &fds); // NOLINT
select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
return FD_ISSET(fd, &fds); // NOLINT
@@ -679,7 +679,7 @@ void ScreenInteractive::Install() {
}
void ScreenInteractive::InstallPipedInputHandling() {
tty_fd_ = STDIN_FILENO;
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
@@ -689,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;
}
@@ -697,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;
}

View File

@@ -1,11 +1,11 @@
// 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 <fcntl.h>
#include <gtest/gtest.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <sys/stat.h>
#include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp"
@@ -47,7 +47,7 @@ class PipedInputTest : public ::testing::Test {
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
close(pipe_fds_[1]); // Close write end to signal EOF
}
}
@@ -94,10 +94,10 @@ TEST_F(PipedInputTest, ExplicitlyDisabled) {
WriteToPipedStdin("test data\n");
screen.Install();
// Stdin should still be the pipe since feature is disabled
EXPECT_FALSE(isatty(STDIN_FILENO));
screen.Uninstall();
}
@@ -107,7 +107,7 @@ TEST_F(PipedInputTest, ExplicitlyEnabled) {
}
auto screen = ScreenInteractive::TerminalOutput();
screen.HandlePipedInput(true); // Explicitly enable
screen.HandlePipedInput(true); // Explicitly enable
auto component = Renderer([] { return text("test"); });
SetupPipedStdin();
@@ -117,12 +117,12 @@ TEST_F(PipedInputTest, ExplicitlyEnabled) {
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));
@@ -137,12 +137,12 @@ TEST_F(PipedInputTest, NormalStdinUnchanged) {
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));
}
@@ -173,12 +173,12 @@ TEST_F(PipedInputTest, MultipleInstallUninstallCycles) {
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");
@@ -191,8 +191,7 @@ TEST_F(PipedInputTest, HandlePipedInputMethodBehavior) {
}
// Test the graceful fallback when /dev/tty is not available
// This test simulates environments like containers where /dev/tty might not
// exist
// This test simulates environments like containers where /dev/tty might not exist
TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) {
auto screen = ScreenInteractive::TerminalOutput();
auto component = Renderer([] { return text("test"); });
@@ -200,23 +199,22 @@ TEST_F(PipedInputTest, GracefulFallbackWhenTtyUnavailable) {
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
// 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
EXPECT_FALSE(isatty(STDIN_FILENO)); // Should still be our test pipe
}
} // namespace
} // 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()
}
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>
class SliderBase : public SliderOption<T>, public ComponentBase {
public:
@@ -61,15 +47,59 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
flexDirection(this->direction) | reflect(gauge_box_) | gauge_color;
}
void OnDirection(Direction pressed) {
if (pressed == this->direction) {
this->value() += this->increment();
return;
void OnLeft() {
switch (this->direction) {
case Direction::Right:
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)) {
this->value() -= this->increment();
return;
void OnRight() {
switch (this->direction) {
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();
if (event == Event::ArrowLeft || event == Event::Character('h')) {
OnDirection(Direction::Left);
OnLeft();
}
if (event == Event::ArrowRight || event == Event::Character('l')) {
OnDirection(Direction::Right);
OnRight();
}
if (event == Event::ArrowUp || event == Event::Character('k')) {
OnDirection(Direction::Up);
OnDown();
}
if (event == Event::ArrowDown || event == Event::Character('j')) {
OnDirection(Direction::Down);
OnUp();
}
this->value() = std::max(this->min(), std::min(this->max(), this->value()));