10 Commits

Author SHA1 Message Date
ArthurSonzogni
dad2eaaa28 Tweak implementation and documentation. 2025-08-17 19:19:06 +02:00
ArthurSonzogni
5c3e3151a5 Update doc 2025-08-17 17:21:24 +02:00
Harri Pehkonen
143b24c6a5 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 14:08:51 +02:00
Sylko Olzscher
40e1fac3d4 Warn against Microsoft <windows.h> min and max macro (#1084)
Warn users they have defined the min/max macros which is not 
compatible with other code from the standard library or FTXUI.

Co-authored-by: Sylko Olzscher <sylko.olzscher@solostec.ch>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2025-08-17 11:18:25 +02:00
Arthur Sonzogni
8ef18ab647 Remove pthread dependency
Some checks are pending
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Waiting to run
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Waiting to run
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Waiting to run
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Waiting to run
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Waiting to run
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Waiting to run
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Waiting to run
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Waiting to run
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Waiting to run
Build / Test modules (llvm, ubuntu-latest) (push) Waiting to run
Documentation / documentation (push) Waiting to run
2025-08-16 18:40:50 +02:00
tattwamasi
994915dbb9 Add ftxui convenience/umbrella module to cmake rules to fix #1083 (#1085)
Some checks failed
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
* Add the umbrella module ftxui to the cmake module build.

* Update cpp20 modules documentation.
2025-07-27 11:39:46 +02:00
Ivan Deyna
3b359e8cd7 #1078: Fix Examples section link (#1079)
Some checks failed
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
2025-07-10 13:22:04 +02:00
Mirion
1073ba414d Remove redundant member from ButtonBase (#1076)
Some checks failed
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
2025-07-08 08:55:37 +02:00
Arthur Sonzogni
b78b97056b Stop using Sender/Receiver in TerminalInputParser. (#1073)
Some checks failed
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
Stop using Sender/Receiver in TerminalInputParser.

This will help removing usage of thread.

At some point, my goal is to have an initialization step when installing
the ScreenInteractive so that we can provide the terminal ID
synchronously without losing some events. This will help with:
https://github.com/ArthurSonzogni/FTXUI/pull/1069
2025-07-02 15:23:01 +02:00
Zane Zhou
68fc9b1212 Fix ScreenInteractive::FixedSize screen stomps on the history terminal output (#1064)
Some checks failed
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (cl, cl, windows-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (clang, clang++, ubuntu-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, macos-latest) (push) Has been cancelled
Build / Bazel, ${{ matrix.cxx }}, ${{ matrix.os }} (gcc, g++, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (cl, Windows MSVC, windows-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (gcc, Linux GCC, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, Linux Clang, ubuntu-latest) (push) Has been cancelled
Build / CMake, ${{ matrix.compiler }}, ${{ matrix.os }} (llvm, llvm-cov gcov, MacOS clang, macos-latest) (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2025-06-20 15:59:36 +02:00
60 changed files with 1561 additions and 645 deletions

View File

@@ -1,3 +1,5 @@
common --enable_bzlmod
build --features=layering_check build --features=layering_check
build --enable_bzlmod build --enable_bzlmod

View File

@@ -2,3 +2,6 @@
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html # http://clang.llvm.org/docs/ClangFormatStyleOptions.html
BasedOnStyle: Chromium BasedOnStyle: Chromium
Standard: Cpp11 Standard: Cpp11
InsertBraces: true
InsertNewlineAtEOF: true

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ out/
!doc/**/*.html !doc/**/*.html
!doc/**/*.xml !doc/**/*.xml
!doc/**/*.md !doc/**/*.md
!doc/*.md
# examples directory: # examples directory:
!examples/**/*.cpp !examples/**/*.cpp

View File

@@ -13,7 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
load(":bazel/ftxui.bzl", "ftxui_cc_library") load(":bazel/ftxui.bzl", "ftxui_cc_library")
load(":bazel/ftxui.bzl", "generate_examples") load(":bazel/ftxui.bzl", "generate_examples")
load(":bazel/ftxui.bzl", "windows_copts") load(":bazel/ftxui.bzl", "windows_copts")
load(":bazel/ftxui.bzl", "pthread_linkopts")
# A meta target depending on all of the ftxui submodules. # A meta target depending on all of the ftxui submodules.
# Note that component depends on dom and screen, so ftxui is just an alias for # Note that component depends on dom and screen, so ftxui is just an alias for
@@ -159,11 +158,18 @@ ftxui_cc_library(
"src/ftxui/component/resizable_split.cpp", "src/ftxui/component/resizable_split.cpp",
"src/ftxui/component/screen_interactive.cpp", "src/ftxui/component/screen_interactive.cpp",
"src/ftxui/component/slider.cpp", "src/ftxui/component/slider.cpp",
"src/ftxui/component/task.cpp",
"src/ftxui/component/task_internal.hpp",
"src/ftxui/component/task_queue.cpp",
"src/ftxui/component/task_queue.hpp",
"src/ftxui/component/task_runner.cpp",
"src/ftxui/component/task_runner.hpp",
"src/ftxui/component/terminal_input_parser.cpp", "src/ftxui/component/terminal_input_parser.cpp",
"src/ftxui/component/terminal_input_parser.hpp", "src/ftxui/component/terminal_input_parser.hpp",
"src/ftxui/component/util.cpp", "src/ftxui/component/util.cpp",
"src/ftxui/component/window.cpp", "src/ftxui/component/window.cpp",
# Private header from ftxui:dom. # Private header from ftxui:dom.
"src/ftxui/dom/node_decorator.hpp", "src/ftxui/dom/node_decorator.hpp",
@@ -184,7 +190,6 @@ ftxui_cc_library(
"include/ftxui/component/screen_interactive.hpp", "include/ftxui/component/screen_interactive.hpp",
"include/ftxui/component/task.hpp", "include/ftxui/component/task.hpp",
], ],
linkopts = pthread_linkopts(),
deps = [ deps = [
":dom", ":dom",
":screen", ":screen",
@@ -207,7 +212,6 @@ cc_test(
"src/ftxui/component/menu_test.cpp", "src/ftxui/component/menu_test.cpp",
"src/ftxui/component/modal_test.cpp", "src/ftxui/component/modal_test.cpp",
"src/ftxui/component/radiobox_test.cpp", "src/ftxui/component/radiobox_test.cpp",
"src/ftxui/component/receiver_test.cpp",
"src/ftxui/component/resizable_split_test.cpp", "src/ftxui/component/resizable_split_test.cpp",
"src/ftxui/component/slider_test.cpp", "src/ftxui/component/slider_test.cpp",
"src/ftxui/component/terminal_input_parser_test.cpp", "src/ftxui/component/terminal_input_parser_test.cpp",

View File

@@ -24,6 +24,11 @@ Next
import ftxui.util; import ftxui.util;
``` ```
Thanks @mikomikotaishi for PR #1015. Thanks @mikomikotaishi for PR #1015.
- Remove dependency on 'pthread'.
### Component
- Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal
output. Thanks @zozowell in #1064.
6.1.9 (2025-05-07) 6.1.9 (2025-05-07)

View File

@@ -144,26 +144,20 @@ add_library(component
src/ftxui/component/resizable_split.cpp src/ftxui/component/resizable_split.cpp
src/ftxui/component/screen_interactive.cpp src/ftxui/component/screen_interactive.cpp
src/ftxui/component/slider.cpp src/ftxui/component/slider.cpp
src/ftxui/component/task.cpp
src/ftxui/component/task_internal.hpp
src/ftxui/component/task_queue.cpp
src/ftxui/component/task_queue.hpp
src/ftxui/component/task_runner.cpp
src/ftxui/component/task_runner.hpp
src/ftxui/component/terminal_input_parser.cpp src/ftxui/component/terminal_input_parser.cpp
src/ftxui/component/terminal_input_parser.hpp src/ftxui/component/terminal_input_parser.hpp
src/ftxui/component/util.cpp src/ftxui/component/util.cpp
src/ftxui/component/window.cpp src/ftxui/component/window.cpp
) )
target_link_libraries(dom target_link_libraries(dom PUBLIC screen)
PUBLIC screen target_link_libraries(component PUBLIC dom)
)
target_link_libraries(component
PUBLIC dom
)
if (NOT EMSCRIPTEN)
find_package(Threads)
target_link_libraries(component
PUBLIC Threads::Threads
)
endif()
include(cmake/ftxui_set_options.cmake) include(cmake/ftxui_set_options.cmake)
ftxui_set_options(screen) ftxui_set_options(screen)

View File

@@ -18,7 +18,7 @@
<br/> <br/>
<a href="https://arthursonzogni.github.io/FTXUI/">Documentation</a> · <a href="https://arthursonzogni.github.io/FTXUI/">Documentation</a> ·
<a href="https://github.com/ArthurSonzogni/FTXUI/issues">Report a Bug</a> · <a href="https://github.com/ArthurSonzogni/FTXUI/issues">Report a Bug</a> ·
<a href="https://arthursonzogni.github.io/FTXUI/examples.html">Examples</a> . <a href="https://arthursonzogni.github.io/FTXUI/examples/">Examples</a> .
<a href="https://github.com/ArthurSonzogni/FTXUI/issues">Request Feature</a> · <a href="https://github.com/ArthurSonzogni/FTXUI/issues">Request Feature</a> ·
<a href="https://github.com/ArthurSonzogni/FTXUI/pulls">Send a Pull Request</a> <a href="https://github.com/ArthurSonzogni/FTXUI/pulls">Send a Pull Request</a>
@@ -378,6 +378,8 @@ 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)
## 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.

View File

@@ -43,16 +43,6 @@ def windows_copts():
"//conditions:default": [], "//conditions:default": [],
}) })
def pthread_linkopts():
return select({
# With MSVC, threading is already built-in (you don't need -pthread.
"@rules_cc//cc/compiler:msvc-cl": [],
"@rules_cc//cc/compiler:clang-cl": [],
"@rules_cc//cc/compiler:clang": ["-pthread"],
"@rules_cc//cc/compiler:gcc": ["-pthread"],
"//conditions:default": ["-pthread"],
})
def ftxui_cc_library( def ftxui_cc_library(
name, name,
srcs = [], srcs = [],

View File

@@ -6,6 +6,7 @@ add_library(ftxui-modules)
target_sources(ftxui-modules target_sources(ftxui-modules
PUBLIC FILE_SET CXX_MODULES FILES PUBLIC FILE_SET CXX_MODULES FILES
src/ftxui/ftxui.cppm
src/ftxui/component.cppm src/ftxui/component.cppm
src/ftxui/component/animation.cppm src/ftxui/component/animation.cppm
src/ftxui/component/captured_mouse.cppm src/ftxui/component/captured_mouse.cppm

View File

@@ -101,6 +101,5 @@ endfunction()
if (EMSCRIPTEN) if (EMSCRIPTEN)
string(APPEND CMAKE_CXX_FLAGS " -s USE_PTHREADS") string(APPEND CMAKE_CXX_FLAGS " -s USE_PTHREADS")
string(APPEND CMAKE_EXE_LINKER_FLAGS " -s ASYNCIFY")
string(APPEND CMAKE_EXE_LINKER_FLAGS " -s PROXY_TO_PTHREAD") string(APPEND CMAKE_EXE_LINKER_FLAGS " -s PROXY_TO_PTHREAD")
endif() endif()

View File

@@ -19,11 +19,10 @@ add_executable(ftxui-tests
src/ftxui/component/menu_test.cpp src/ftxui/component/menu_test.cpp
src/ftxui/component/modal_test.cpp src/ftxui/component/modal_test.cpp
src/ftxui/component/radiobox_test.cpp src/ftxui/component/radiobox_test.cpp
src/ftxui/util/ref_test.cpp
src/ftxui/component/receiver_test.cpp
src/ftxui/component/resizable_split_test.cpp src/ftxui/component/resizable_split_test.cpp
src/ftxui/component/screen_interactive_test.cpp src/ftxui/component/screen_interactive_test.cpp
src/ftxui/component/slider_test.cpp src/ftxui/component/slider_test.cpp
src/ftxui/component/task_test.cpp
src/ftxui/component/terminal_input_parser_test.cpp src/ftxui/component/terminal_input_parser_test.cpp
src/ftxui/component/toggle_test.cpp src/ftxui/component/toggle_test.cpp
src/ftxui/dom/blink_test.cpp src/ftxui/dom/blink_test.cpp
@@ -51,6 +50,7 @@ add_executable(ftxui-tests
src/ftxui/dom/vbox_test.cpp src/ftxui/dom/vbox_test.cpp
src/ftxui/screen/color_test.cpp src/ftxui/screen/color_test.cpp
src/ftxui/screen/string_test.cpp src/ftxui/screen/string_test.cpp
src/ftxui/util/ref_test.cpp
) )
target_link_libraries(ftxui-tests target_link_libraries(ftxui-tests

View File

@@ -12,8 +12,24 @@ FTXUI experimentally supports
compilation times and improve code organization. Each header has a compilation times and improve code organization. Each header has a
corresponding module. corresponding module.
**Example with CMake and Ninja** Use the FTXUI_BUILD_MODULES option to build the FTXUI project itself to provide C++ 20 modules,
for example with CMake and Ninja:
```sh
cmake \
-DCMAKE_GENERATOR=Ninja \
-DFTXUI_BUILD_MODULES=ON \
..
ninja
```
> [!NOTE]
> To use modules, you need a C++20 compatible compiler, CMake version 3.20 or
> higher, and use a compatible generator like Ninja. Note that Makefile
> generators **do not support modules**.
Then, in your own code you can consume the modules and code as normal:
```cpp ```cpp
import ftxui; import ftxui;
@@ -26,18 +42,25 @@ int main() {
} }
``` ```
```sh Note, the `ftxui` convenience module which simply pulls together all the modules:
cmake \
-DCMAKE_GENERATOR=Ninja \
-DFTXUI_BUILD_MODULES=ON \
..
ninja ```cpp
export import ftxui.component;
export import ftxui.dom;
export import ftxui.screen;
export import ftxui.util;
```
You can instead import only the module(s) you need if desired.
To properly find and link the modules with CMake, use `target_link_libraries` to get the right
compiler, linker, etc. flags.
```cmake
target_link_libraries(my_executable
#...whatever...
PRIVATE ftxui::modules
)
``` ```
> [!NOTE]
> To use modules, you need a C++20 compatible compiler, CMake version 3.20 or
> higher, and use a compatible generator like Ninja. Note that Makefile
> generators **do not support modules**.
### Module list ### Module list

58
doc/posix_pipe.md Normal file
View File

@@ -0,0 +1,58 @@
# 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.
**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);
```
## 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);
```

View File

@@ -15,15 +15,11 @@ add_subdirectory(component)
add_subdirectory(dom) add_subdirectory(dom)
if (EMSCRIPTEN) if (EMSCRIPTEN)
string(APPEND CMAKE_EXE_LINKER_FLAGS " -s ALLOW_MEMORY_GROWTH=1")
target_link_options(component PUBLIC "SHELL: -s ALLOW_MEMORY_GROWTH=1")
get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES) get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES)
foreach(file foreach(file
"index.html" "index.html"
"index.mjs" "index.mjs"
"index.css" "index.css"
"sw.js"
"run_webassembly.py") "run_webassembly.py")
configure_file(${file} ${file}) configure_file(${file} ${file})
endforeach(file) endforeach(file)

View File

@@ -1,9 +1,64 @@
#include "ftxui/component/component.hpp" // Copyright 2020 Arthur Sonzogni. All rights reserved.
#include "ftxui/component/screen_interactive.hpp" // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <memory> // for shared_ptr, __shared_ptr_access
#include <string> // for operator+, to_string
#include "ftxui/component/captured_mouse.hpp" // for ftxui
#include "ftxui/component/component.hpp" // for Button, Horizontal, Renderer
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
#include "ftxui/dom/elements.hpp" // for separator, gauge, text, Element, operator|, vbox, border
using namespace ftxui;
// This is a helper function to create a button with a custom style.
// The style is defined by a lambda function that takes an EntryState and
// returns an Element.
// We are using `center` to center the text inside the button, then `border` to
// add a border around the button, and finally `flex` to make the button fill
// the available space.
ButtonOption Style() {
auto option = ButtonOption::Animated();
option.transform = [](const EntryState& s) {
auto element = text(s.label);
if (s.focused) {
element |= bold;
}
return element | center | borderEmpty | flex;
};
return option;
}
int main() { int main() {
auto screen = ftxui::ScreenInteractive::Fullscreen(); int value = 50;
auto testComponent = ftxui::Renderer([](){return ftxui::text("test Component");});
screen.Loop(testComponent); // clang-format off
auto btn_dec_01 = Button("-1", [&] { value += 1; }, Style());
auto btn_inc_01 = Button("+1", [&] { value -= 1; }, Style());
auto btn_dec_10 = Button("-10", [&] { value -= 10; }, Style());
auto btn_inc_10 = Button("+10", [&] { value += 10; }, Style());
// clang-format on
// The tree of components. This defines how to navigate using the keyboard.
// The selected `row` is shared to get a grid layout.
int row = 0;
auto buttons = Container::Vertical({
Container::Horizontal({btn_dec_01, btn_inc_01}, &row) | flex,
Container::Horizontal({btn_dec_10, btn_inc_10}, &row) | flex,
});
// Modify the way to render them on screen:
auto component = Renderer(buttons, [&] {
return vbox({
text("value = " + std::to_string(value)),
separator(),
buttons->Render() | flex,
}) |
flex | border;
});
auto screen = ScreenInteractive::FitComponent();
screen.Loop(component);
return 0; return 0;
} }

View File

@@ -133,8 +133,9 @@ int main() {
float dy = 50.f; float dy = 50.f;
ys[x] = int(dy + 20 * cos(dx * 0.14) + 10 * sin(dx * 0.42)); ys[x] = int(dy + 20 * cos(dx * 0.14) + 10 * sin(dx * 0.42));
} }
for (int x = 1; x < 99; x++) for (int x = 1; x < 99; x++) {
c.DrawPointLine(x, ys[x], x + 1, ys[x + 1]); c.DrawPointLine(x, ys[x], x + 1, ys[x + 1]);
}
return canvas(std::move(c)); return canvas(std::move(c));
}); });

View File

@@ -82,10 +82,12 @@ int main() {
size(WIDTH, EQUAL, dimx) | size(HEIGHT, EQUAL, dimy) | size(WIDTH, EQUAL, dimx) | size(HEIGHT, EQUAL, dimy) |
bgcolor(Color::HSV(index * 25, 255, 255)) | bgcolor(Color::HSV(index * 25, 255, 255)) |
color(Color::Black); color(Color::Black);
if (element_xflex_grow) if (element_xflex_grow) {
element = element | xflex_grow; element = element | xflex_grow;
if (element_yflex_grow) }
if (element_yflex_grow) {
element = element | yflex_grow; element = element | yflex_grow;
}
return element; return element;
}; };
@@ -119,10 +121,12 @@ int main() {
group = group | notflex; group = group | notflex;
if (!group_xflex_grow) if (!group_xflex_grow) {
group = hbox(group, filler()); group = hbox(group, filler());
if (!group_yflex_grow) }
if (!group_yflex_grow) {
group = vbox(group, filler()); group = vbox(group, filler());
}
group = group | flex; group = group | flex;
return group; return group;

View File

@@ -6,6 +6,7 @@
#include <atomic> // for atomic #include <atomic> // for atomic
#include <chrono> // for operator""s, chrono_literals #include <chrono> // for operator""s, chrono_literals
#include <cmath> // for sin #include <cmath> // for sin
#include <ftxui/component/loop.hpp>
#include <functional> // for ref, reference_wrapper, function #include <functional> // for ref, reference_wrapper, function
#include <memory> // for allocator, shared_ptr, __shared_ptr_access #include <memory> // for allocator, shared_ptr, __shared_ptr_access
#include <string> // for string, basic_string, char_traits, operator+, to_string #include <string> // for string, basic_string, char_traits, operator+, to_string
@@ -269,7 +270,7 @@ int main() {
auto spinner_tab_renderer = Renderer([&] { auto spinner_tab_renderer = Renderer([&] {
Elements entries; Elements entries;
for (int i = 0; i < 22; ++i) { for (int i = 0; i < 22; ++i) {
entries.push_back(spinner(i, shift / 2) | bold | entries.push_back(spinner(i, shift / 5) | bold |
size(WIDTH, GREATER_THAN, 2) | border); size(WIDTH, GREATER_THAN, 2) | border);
} }
return hflow(std::move(entries)); return hflow(std::move(entries));
@@ -512,24 +513,20 @@ int main() {
}); });
}); });
std::atomic<bool> refresh_ui_continue = true; Loop loop(&screen, main_renderer);
std::thread refresh_ui([&] { while (!loop.HasQuitted()) {
while (refresh_ui_continue) { // Update the state of the application.
using namespace std::chrono_literals; shift++;
std::this_thread::sleep_for(0.05s);
// The |shift| variable belong to the main thread. `screen.Post(task)`
// will execute the update on the thread where |screen| lives (e.g. the
// main thread). Using `screen.Post(task)` is threadsafe.
screen.Post([&] { shift++; });
// After updating the state, request a new frame to be drawn. This is done
// by simulating a new "custom" event to be handled.
screen.Post(Event::Custom);
}
});
screen.Loop(main_renderer); // Request a new frame to be drawn.
refresh_ui_continue = false; screen.RequestAnimationFrame();
refresh_ui.join();
// Execute events, and draw the next frame.
loop.RunOnce();
// Sleep for a short duration to control the frame rate (60 FPS).
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 60));
}
return 0; return 0;
} }

View File

@@ -22,10 +22,12 @@ MenuEntryOption Colored(ftxui::Color c) {
option.transform = [c](EntryState state) { option.transform = [c](EntryState state) {
state.label = (state.active ? "> " : " ") + state.label; state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label) | color(c); Element e = text(state.label) | color(c);
if (state.focused) if (state.focused) {
e = e | inverted; e = e | inverted;
if (state.active) }
if (state.active) {
e = e | bold; e = e | bold;
}
return e; return e;
}; };
return option; return option;

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries; std::vector<std::string> entries;
int selected = 0; int selected = 0;
for (int i = 0; i < 30; ++i) for (int i = 0; i < 30; ++i) {
entries.push_back("Entry " + std::to_string(i)); entries.push_back("Entry " + std::to_string(i));
}
auto radiobox = Menu(&entries, &selected); auto radiobox = Menu(&entries, &selected);
auto renderer = Renderer(radiobox, [&] { auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame | return radiobox->Render() | vscroll_indicator | frame |

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries; std::vector<std::string> entries;
int selected = 0; int selected = 0;
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i) {
entries.push_back(std::to_string(i)); entries.push_back(std::to_string(i));
}
auto radiobox = Menu(&entries, &selected, MenuOption::Horizontal()); auto radiobox = Menu(&entries, &selected, MenuOption::Horizontal());
auto renderer = Renderer( auto renderer = Renderer(
radiobox, [&] { return radiobox->Render() | hscroll_indicator | frame; }); radiobox, [&] { return radiobox->Render() | hscroll_indicator | frame; });

View File

@@ -116,10 +116,12 @@ Component VMenu1(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
state.label = (state.active ? "> " : " ") + state.label; state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label); Element e = text(state.label);
if (state.focused) if (state.focused) {
e = e | bgcolor(Color::Blue); e = e | bgcolor(Color::Blue);
if (state.active) }
if (state.active) {
e = e | bold; e = e | bold;
}
return e; return e;
}; };
return Menu(entries, selected, option); return Menu(entries, selected, option);
@@ -130,10 +132,12 @@ Component VMenu2(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
state.label += (state.active ? " <" : " "); state.label += (state.active ? " <" : " ");
Element e = hbox(filler(), text(state.label)); Element e = hbox(filler(), text(state.label));
if (state.focused) if (state.focused) {
e = e | bgcolor(Color::Red); e = e | bgcolor(Color::Red);
if (state.active) }
if (state.active) {
e = e | bold; e = e | bold;
}
return e; return e;
}; };
return Menu(entries, selected, option); return Menu(entries, selected, option);
@@ -144,13 +148,16 @@ Component VMenu3(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
Element e = state.active ? text("[" + state.label + "]") Element e = state.active ? text("[" + state.label + "]")
: text(" " + state.label + " "); : text(" " + state.label + " ");
if (state.focused) if (state.focused) {
e = e | bold; e = e | bold;
}
if (state.focused) if (state.focused) {
e = e | color(Color::Blue); e = e | color(Color::Blue);
if (state.active) }
if (state.active) {
e = e | bold; e = e | bold;
}
return e; return e;
}; };
return Menu(entries, selected, option); return Menu(entries, selected, option);
@@ -245,10 +252,12 @@ Component HMenu5(std::vector<std::string>* entries, int* selected) {
animation::easing::ElasticOut); animation::easing::ElasticOut);
option.entries_option.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
Element e = text(state.label) | hcenter | flex; Element e = text(state.label) | hcenter | flex;
if (state.active && state.focused) if (state.active && state.focused) {
e = e | bold; e = e | bold;
if (!state.focused && !state.active) }
if (!state.focused && !state.active) {
e = e | dim; e = e | dim;
}
return e; return e;
}; };
option.underline.color_inactive = Color::Default; option.underline.color_inactive = Color::Default;

View File

@@ -20,8 +20,9 @@ using namespace ftxui;
Component DummyComponent(int id) { Component DummyComponent(int id) {
return Renderer([id](bool focused) { return Renderer([id](bool focused) {
auto t = text("component " + std::to_string(id)); auto t = text("component " + std::to_string(id));
if (focused) if (focused) {
t = t | inverted; t = t | inverted;
}
return t; return t;
}); });
} }

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries; std::vector<std::string> entries;
int selected = 0; int selected = 0;
for (int i = 0; i < 30; ++i) for (int i = 0; i < 30; ++i) {
entries.push_back("RadioBox " + std::to_string(i)); entries.push_back("RadioBox " + std::to_string(i));
}
auto radiobox = Radiobox(&entries, &selected); auto radiobox = Radiobox(&entries, &selected);
auto renderer = Renderer(radiobox, [&] { auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame | return radiobox->Render() | vscroll_indicator | frame |

View File

@@ -19,10 +19,11 @@ int main() {
// 1. Example of focusable renderer: // 1. Example of focusable renderer:
auto renderer_focusable = Renderer([](bool focused) { auto renderer_focusable = Renderer([](bool focused) {
if (focused) if (focused) {
return text("FOCUSABLE RENDERER()") | center | bold | border; return text("FOCUSABLE RENDERER()") | center | bold | border;
else } else {
return text(" Focusable renderer() ") | center | border; return text(" Focusable renderer() ") | center | border;
}
}); });
// 2. Examples of a non focusable renderer. // 2. Examples of a non focusable renderer.
@@ -33,10 +34,11 @@ int main() {
// 3. Renderer can wrap other components to redefine their Render() function. // 3. Renderer can wrap other components to redefine their Render() function.
auto button = Button("Wrapped quit button", screen.ExitLoopClosure()); auto button = Button("Wrapped quit button", screen.ExitLoopClosure());
auto renderer_wrap = Renderer(button, [&] { auto renderer_wrap = Renderer(button, [&] {
if (button->Focused()) if (button->Focused()) {
return button->Render() | bold | color(Color::Red); return button->Render() | bold | color(Color::Red);
else } else {
return button->Render(); return button->Render();
}
}); });
// Let's renderer everyone: // Let's renderer everyone:

View File

@@ -32,10 +32,12 @@ int main() {
// Plot a function: // Plot a function:
std::vector<int> ys(100); std::vector<int> ys(100);
for (int x = 0; x < 100; x++) for (int x = 0; x < 100; x++) {
ys[x] = int(80 + 20 * cos(x * 0.2)); ys[x] = int(80 + 20 * cos(x * 0.2));
for (int x = 0; x < 99; x++) }
for (int x = 0; x < 99; x++) {
c.DrawPointLine(x, ys[x], x + 1, ys[x + 1], Color::Red); c.DrawPointLine(x, ys[x], x + 1, ys[x + 1], Color::Red);
}
auto document = canvas(&c) | border; auto document = canvas(&c) | border;

View File

@@ -86,8 +86,9 @@ int main() {
auto render = [&]() { auto render = [&]() {
std::vector<Element> entries; std::vector<Element> entries;
for (auto& task : displayed_task) for (auto& task : displayed_task) {
entries.push_back(renderTask(task)); entries.push_back(renderTask(task));
}
return vbox({ return vbox({
// List of tasks. // List of tasks.
@@ -138,8 +139,9 @@ int main() {
std::this_thread::sleep_for(0.01s); std::this_thread::sleep_for(0.01s);
// Exit // Exit
if (nb_active + nb_queued == 0) if (nb_active + nb_queued == 0) {
break; break;
}
// Update the model for the next frame. // Update the model for the next frame.
updateModel(); updateModel();

View File

@@ -21,8 +21,9 @@ int main() {
for (int index = 0; index < 200; ++index) { for (int index = 0; index < 200; ++index) {
std::vector<Element> entries; std::vector<Element> entries;
for (int i = 0; i < 23; ++i) { for (int i = 0; i < 23; ++i) {
if (i != 0) if (i != 0) {
entries.push_back(separator()); entries.push_back(separator());
}
entries.push_back( // entries.push_back( //
hbox({ hbox({
text(std::to_string(i)) | size(WIDTH, EQUAL, 2), text(std::to_string(i)) | size(WIDTH, EQUAL, 2),

View File

@@ -55,7 +55,7 @@ const stdout = code => {
const stderr = code => { const stderr = code => {
if (code == 0 || code == 10) { if (code == 0 || code == 10) {
console.error(String.fromCodePoint(...stderr_buffer)); console.error(String.fromCodePoint(...stderr_buffer));
stderr_buffer = []; stderr_buffer.length = 0;
} else { } else {
stderr_buffer.push(code) stderr_buffer.push(code)
} }
@@ -89,9 +89,6 @@ window.Module = {
const resize_observer = new ResizeObserver(resize_handler); const resize_observer = new ResizeObserver(resize_handler);
resize_observer.observe(term_element); resize_observer.observe(term_element);
resize_handler(); resize_handler();
// Disable scrollbar
//term.write('\x1b[?47h')
}, },
}; };

View File

@@ -8,6 +8,7 @@
#include <memory> // for make_shared, shared_ptr #include <memory> // for make_shared, shared_ptr
#include <utility> // for forward #include <utility> // for forward
#include <ftxui/util/warn_windows_macro.hpp>
#include "ftxui/component/component_base.hpp" // for Component, Components #include "ftxui/component/component_base.hpp" // for Component, Components
#include "ftxui/component/component_options.hpp" // for ButtonOption, CheckboxOption, MenuOption #include "ftxui/component/component_options.hpp" // for ButtonOption, CheckboxOption, MenuOption
#include "ftxui/dom/elements.hpp" // for Element #include "ftxui/dom/elements.hpp" // for Element

View File

@@ -9,6 +9,7 @@
#include <ftxui/dom/direction.hpp> // for Direction, Direction::Left, Direction::Right, Direction::Down #include <ftxui/dom/direction.hpp> // for Direction, Direction::Left, Direction::Right, Direction::Down
#include <ftxui/dom/elements.hpp> // for Element, separator #include <ftxui/dom/elements.hpp> // for Element, separator
#include <ftxui/util/ref.hpp> // for Ref, ConstRef, StringRef #include <ftxui/util/ref.hpp> // for Ref, ConstRef, StringRef
#include <ftxui/util/warn_windows_macro.hpp>
#include <functional> // for function #include <functional> // for function
#include <string> // for string #include <string> // for string

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
@@ -14,6 +15,8 @@
namespace ftxui { namespace ftxui {
// Deprecated
//
// Usage: // Usage:
// //
// Initialization: // Initialization:
@@ -39,17 +42,24 @@ namespace ftxui {
// Receiver::Receive() returns true when there are no more senders. // Receiver::Receive() returns true when there are no more senders.
// clang-format off // clang-format off
// Deprecated:
template<class T> class SenderImpl; template<class T> class SenderImpl;
// Deprecated:
template<class T> class ReceiverImpl; template<class T> class ReceiverImpl;
// Deprecated:
// Deprecated:
template<class T> using Sender = std::unique_ptr<SenderImpl<T>>; template<class T> using Sender = std::unique_ptr<SenderImpl<T>>;
// Deprecated:
template<class T> using Receiver = std::unique_ptr<ReceiverImpl<T>>; template<class T> using Receiver = std::unique_ptr<ReceiverImpl<T>>;
// Deprecated:
template<class T> Receiver<T> MakeReceiver(); template<class T> Receiver<T> MakeReceiver();
// clang-format on // clang-format on
// ---- Implementation part ---- // ---- Implementation part ----
template <class T> template <class T>
// Deprecated:
class SenderImpl { class SenderImpl {
public: public:
SenderImpl(const SenderImpl&) = delete; SenderImpl(const SenderImpl&) = delete;

View File

@@ -5,12 +5,9 @@
#define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP #define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#include <atomic> // for atomic #include <atomic> // for atomic
#include <ftxui/component/receiver.hpp> // for Receiver, Sender
#include <functional> // for function #include <functional> // for function
#include <memory> // for shared_ptr #include <memory> // for shared_ptr
#include <string> // for string #include <string> // for string
#include <thread> // for thread
#include <variant> // for variant
#include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -27,6 +24,10 @@ struct Event;
using Component = std::shared_ptr<ComponentBase>; using Component = std::shared_ptr<ComponentBase>;
class ScreenInteractivePrivate; class ScreenInteractivePrivate;
namespace task {
class TaskRunner;
}
/// @brief ScreenInteractive is a `Screen` that can handle events, run a main /// @brief ScreenInteractive is a `Screen` that can handle events, run a main
/// loop, and manage components. /// loop, and manage components.
/// ///
@@ -41,8 +42,12 @@ class ScreenInteractive : public Screen {
static ScreenInteractive FitComponent(); static ScreenInteractive FitComponent();
static ScreenInteractive TerminalOutput(); static ScreenInteractive TerminalOutput();
// Destructor.
~ScreenInteractive() override;
// 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();
@@ -96,8 +101,14 @@ class ScreenInteractive : public Screen {
void Draw(Component component); void Draw(Component component);
void ResetCursorPosition(); void ResetCursorPosition();
void InstallPipedInputHandling();
void Signal(int signal); void Signal(int signal);
void FetchTerminalEvents();
void PostAnimationTask();
ScreenInteractive* suspended_screen_ = nullptr; ScreenInteractive* suspended_screen_ = nullptr;
enum class Dimension { enum class Dimension {
FitComponent, FitComponent,
@@ -105,30 +116,27 @@ class ScreenInteractive : public Screen {
Fullscreen, Fullscreen,
TerminalOutput, TerminalOutput,
}; };
Dimension dimension_ = Dimension::Fixed; ScreenInteractive(Dimension dimension,
bool use_alternative_screen_ = false; int dimx,
ScreenInteractive(int dimx,
int dimy, int dimy,
Dimension dimension,
bool use_alternative_screen); bool use_alternative_screen);
bool track_mouse_ = true; const Dimension dimension_;
const bool use_alternative_screen_;
Sender<Task> task_sender_; bool track_mouse_ = true;
Receiver<Task> task_receiver_;
std::string set_cursor_position; std::string set_cursor_position;
std::string reset_cursor_position; std::string reset_cursor_position;
std::atomic<bool> quit_{false}; std::atomic<bool> quit_{false};
std::thread event_listener_;
std::thread animation_listener_;
bool animation_requested_ = false; bool animation_requested_ = false;
animation::TimePoint previous_animation_time_; animation::TimePoint previous_animation_time_;
int cursor_x_ = 1; int cursor_x_ = 1;
int cursor_y_ = 1; int cursor_y_ = 1;
std::uint64_t frame_count_ = 0;
bool mouse_captured = false; bool mouse_captured = false;
bool previous_frame_resized_ = false; bool previous_frame_resized_ = false;
@@ -137,6 +145,9 @@ 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;
// Piped input handling state (POSIX only)
bool handle_piped_input_ = true;
// 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;
@@ -156,8 +167,14 @@ class ScreenInteractive : public Screen {
std::unique_ptr<Selection> selection_; std::unique_ptr<Selection> selection_;
std::function<void()> selection_on_change_; std::function<void()> selection_on_change_;
// PIMPL private implementation idiom (Pimpl).
struct Internal;
std::unique_ptr<Internal> internal_;
friend class Loop; friend class Loop;
Component component_;
public: public:
class Private { class Private {
public: public:

View File

@@ -12,7 +12,6 @@
namespace ftxui { namespace ftxui {
/// @brief FlexboxConfig is a configuration structure that defines the layout /// @brief FlexboxConfig is a configuration structure that defines the layout
/// properties for a flexbox container. /// properties for a flexbox container.
// //

View File

@@ -20,6 +20,9 @@ class Image {
Image() = delete; Image() = delete;
Image(int dimx, int dimy); Image(int dimx, int dimy);
// Destructor:
virtual ~Image() = default;
// Access a character in the grid at a given position. // Access a character in the grid at a given position.
std::string& at(int x, int y); std::string& at(int x, int y);
const std::string& at(int x, int y) const; const std::string& at(int x, int y) const;

View File

@@ -11,7 +11,6 @@
#include "ftxui/screen/image.hpp" // for Pixel, Image #include "ftxui/screen/image.hpp" // for Pixel, Image
#include "ftxui/screen/terminal.hpp" // for Dimensions #include "ftxui/screen/terminal.hpp" // for Dimensions
#include "ftxui/util/autoreset.hpp" // for AutoReset
namespace ftxui { namespace ftxui {
@@ -31,6 +30,9 @@ class Screen : public Image {
static Screen Create(Dimensions dimension); static Screen Create(Dimensions dimension);
static Screen Create(Dimensions width, Dimensions height); static Screen Create(Dimensions width, Dimensions height);
// Destructor:
~Screen() override = default;
std::string ToString() const; std::string ToString() const;
// Print the Screen on to the terminal. // Print the Screen on to the terminal.

View File

@@ -0,0 +1,18 @@
// 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.
#ifndef FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#define FTXUI_UTIL_WARN_WINDOWS_MACRO_H_
#ifdef min
#error \
"The macro 'min' 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
#ifdef max
#error \
"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_H_

View File

@@ -139,7 +139,6 @@ class ButtonBase : public ComponentBase, public ButtonOption {
private: private:
bool mouse_hover_ = false; bool mouse_hover_ = false;
Box box_; Box box_;
ButtonOption option_;
float animation_background_ = 0; float animation_background_ = 0;
float animation_foreground_ = 0; float animation_foreground_ = 0;
animation::Animator animator_background_ = animation::Animator animator_background_ =

View File

@@ -2,6 +2,7 @@
// 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 <cassert> #include <cassert>
#include <ftxui/component/event.hpp>
#include <vector> #include <vector>
#include "ftxui/component/component.hpp" #include "ftxui/component/component.hpp"
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
@@ -22,8 +23,9 @@ bool GeneratorBool(const char*& data, size_t& size) {
std::string GeneratorString(const char*& data, size_t& size) { std::string GeneratorString(const char*& data, size_t& size) {
int index = 0; int index = 0;
while (index < size && data[index]) while (index < size && data[index]) {
++index; ++index;
}
auto out = std::string(data, data + index); auto out = std::string(data, data + index);
data += index; data += index;
@@ -39,8 +41,9 @@ std::string GeneratorString(const char*& data, size_t& size) {
} }
int GeneratorInt(const char* data, size_t size) { int GeneratorInt(const char* data, size_t size) {
if (size == 0) if (size == 0) {
return 0; return 0;
}
auto out = int(data[0]); auto out = int(data[0]);
data++; data++;
size--; size--;
@@ -112,8 +115,9 @@ Components GeneratorComponents(const char*& data, size_t& size, int depth);
Component GeneratorComponent(const char*& data, size_t& size, int depth) { Component GeneratorComponent(const char*& data, size_t& size, int depth) {
depth--; depth--;
int value = GeneratorInt(data, size); int value = GeneratorInt(data, size);
if (depth <= 0) if (depth <= 0) {
return Button(GeneratorString(data, size), [] {}); return Button(GeneratorString(data, size), [] {});
}
constexpr int value_max = 19; constexpr int value_max = 19;
value = (value % value_max + value_max) % value_max; value = (value % value_max + value_max) % value_max;
@@ -212,16 +216,17 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
auto screen = auto screen =
Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height)); Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height));
auto event_receiver = MakeReceiver<Task>(); // Generate some events.
{ std::vector<Event> events;
auto parser = TerminalInputParser(event_receiver->MakeSender()); auto parser =
for (size_t i = 0; i < size; ++i) TerminalInputParser([&](const Event& event) { events.push_back(event); });
for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]); parser.Add(data[i]);
} }
Task event; for (const auto& event : events) {
while (event_receiver->Receive(&event)) { component->OnEvent(event);
component->OnEvent(std::get<Event>(event));
auto document = component->Render(); auto document = component->Render();
Render(screen, document); Render(screen, document);
} }

View File

@@ -12,9 +12,15 @@ export module ftxui.component.receiver;
* @brief The FTXUI ftxui:: namespace * @brief The FTXUI ftxui:: namespace
*/ */
export namespace ftxui { export namespace ftxui {
// Deprecated:
using ftxui::SenderImpl; using ftxui::SenderImpl;
// Deprecated:
using ftxui::ReceiverImpl; using ftxui::ReceiverImpl;
// Deprecated:
using ftxui::Sender; using ftxui::Sender;
// Deprecated:
using ftxui::Receiver; using ftxui::Receiver;
// Deprecated:
using ftxui::MakeReceiver; using ftxui::MakeReceiver;
// Deprecated:
} }

View File

@@ -1,81 +0,0 @@
// Copyright 2020 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 <thread> // for thread
#include <utility> // for move
#include "ftxui/component/receiver.hpp"
#include "gtest/gtest.h" // for AssertionResult, Message, Test, TestPartResult, EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE, TEST
// NOLINTBEGIN
namespace ftxui {
TEST(Receiver, Basic) {
auto receiver = MakeReceiver<char>();
auto sender = receiver->MakeSender();
sender->Send('a');
sender->Send('b');
sender->Send('c');
sender.reset();
char a, b, c, d;
EXPECT_TRUE(receiver->Receive(&a));
EXPECT_TRUE(receiver->Receive(&b));
EXPECT_TRUE(receiver->Receive(&c));
EXPECT_FALSE(receiver->Receive(&d));
EXPECT_EQ(a, 'a');
EXPECT_EQ(b, 'b');
EXPECT_EQ(c, 'c');
}
TEST(Receiver, BasicWithThread) {
auto r1 = MakeReceiver<char>();
auto r2 = MakeReceiver<char>();
auto r3 = MakeReceiver<char>();
auto s1 = r1->MakeSender();
auto s2 = r2->MakeSender();
auto s3 = r3->MakeSender();
auto s1_bis = r1->MakeSender();
auto stream = [](Receiver<char> receiver, Sender<char> sender) {
char c;
while (receiver->Receive(&c))
sender->Send(c);
};
// Convert data from a different thread.
auto t12 = std::thread(stream, std::move(r1), std::move(s2));
auto t23 = std::thread(stream, std::move(r2), std::move(s3));
// Send some data.
s1->Send('1');
s1_bis->Send('2');
s1->Send('3');
s1_bis->Send('4');
// Close the stream.
s1.reset();
s1_bis.reset();
char c;
EXPECT_TRUE(r3->Receive(&c));
EXPECT_EQ(c, '1');
EXPECT_TRUE(r3->Receive(&c));
EXPECT_EQ(c, '2');
EXPECT_TRUE(r3->Receive(&c));
EXPECT_EQ(c, '3');
EXPECT_TRUE(r3->Receive(&c));
EXPECT_EQ(c, '4');
EXPECT_FALSE(r3->Receive(&c));
// Thread will end at the end of the stream.
t12.join();
t23.join();
}
} // namespace ftxui
// NOLINTEND

View File

@@ -19,7 +19,6 @@
#include <string> #include <string>
#include <thread> // for thread, sleep_for #include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore #include <tuple> // for _Swallow_assign, ignore
#include <type_traits> // for decay_t
#include <utility> // for move, swap #include <utility> // for move, swap
#include <variant> // for visit, variant #include <variant> // for visit, variant
#include <vector> // for vector #include <vector> // for vector
@@ -28,13 +27,12 @@
#include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event #include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/loop.hpp" // for Loop #include "ftxui/component/loop.hpp" // for Loop
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver #include "ftxui/component/task_runner.hpp"
#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
#include "ftxui/dom/node.hpp" // for Node, Render #include "ftxui/dom/node.hpp" // for Node, Render
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/screen/pixel.hpp" // for Pixel
#include "ftxui/screen/terminal.hpp" // for Dimensions, Size #include "ftxui/screen/terminal.hpp" // for Dimensions, Size
#include "ftxui/screen/util.hpp" // for util::clamp #include "ftxui/screen/util.hpp" // for util::clamp
#include "ftxui/util/autoreset.hpp" // for AutoReset
#if defined(_WIN32) #if defined(_WIN32)
#define DEFINE_CONSOLEV2_PROPERTIES #define DEFINE_CONSOLEV2_PROPERTIES
@@ -47,9 +45,11 @@
#error Must be compiled in UNICODE mode #error Must be compiled in UNICODE mode
#endif #endif
#else #else
#include <fcntl.h>
#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval #include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME #include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
#include <unistd.h> // for STDIN_FILENO, read #include <unistd.h> // for STDIN_FILENO, read
#include <cerrno>
#endif #endif
// Quick exit is missing in standard CLang headers // Quick exit is missing in standard CLang headers
@@ -59,6 +59,20 @@
namespace ftxui { namespace ftxui {
struct ScreenInteractive::Internal {
// Convert char to Event.
TerminalInputParser terminal_input_parser;
task::TaskRunner task_runner;
// The last time a character was received.
std::chrono::time_point<std::chrono::steady_clock> last_char_time =
std::chrono::steady_clock::now();
explicit Internal(std::function<void(Event)> out)
: terminal_input_parser(std::move(out)) {}
};
namespace animation { namespace animation {
void RequestAnimationFrame() { void RequestAnimationFrame() {
auto* screen = ScreenInteractive::Active(); auto* screen = ScreenInteractive::Active();
@@ -82,73 +96,9 @@ constexpr int timeout_milliseconds = 20;
timeout_milliseconds * 1000; timeout_milliseconds * 1000;
#if defined(_WIN32) #if defined(_WIN32)
void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto console = GetStdHandle(STD_INPUT_HANDLE);
auto parser = TerminalInputParser(out->Clone());
while (!*quit) {
// Throttle ReadConsoleInput by waiting 250ms, this wait function will
// return if there is input in the console.
auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
if (wait_result == WAIT_TIMEOUT) {
parser.Timeout(timeout_milliseconds);
continue;
}
DWORD number_of_events = 0;
if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
continue;
if (number_of_events <= 0)
continue;
std::vector<INPUT_RECORD> records{number_of_events};
DWORD number_of_events_read = 0;
ReadConsoleInput(console, records.data(), (DWORD)records.size(),
&number_of_events_read);
records.resize(number_of_events_read);
for (const auto& r : records) {
switch (r.EventType) {
case KEY_EVENT: {
auto key_event = r.Event.KeyEvent;
// ignore UP key events
if (key_event.bKeyDown == FALSE)
continue;
std::wstring wstring;
wstring += key_event.uChar.UnicodeChar;
for (auto it : to_string(wstring)) {
parser.Add(it);
}
} break;
case WINDOW_BUFFER_SIZE_EVENT:
out->Send(Event::Special({0}));
break;
case MENU_EVENT:
case FOCUS_EVENT:
case MOUSE_EVENT:
// TODO(mauve): Implement later.
break;
}
}
}
}
#elif defined(__EMSCRIPTEN__) #elif defined(__EMSCRIPTEN__)
#include <emscripten.h> #include <emscripten.h>
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = TerminalInputParser(std::move(out));
char c;
while (!*quit) {
while (read(STDIN_FILENO, &c, 1), c)
parser.Add(c);
emscripten_sleep(1);
parser.Timeout(1);
}
}
extern "C" { extern "C" {
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
void ftxui_on_resize(int columns, int rows) { void ftxui_on_resize(int columns, int rows) {
@@ -162,8 +112,8 @@ void ftxui_on_resize(int columns, int rows) {
#else // POSIX (Linux & Mac) #else // POSIX (Linux & Mac)
int CheckStdinReady(int usec_timeout) { int CheckStdinReady() {
timeval tv = {0, usec_timeout}; // NOLINT timeval tv = {0, 0}; // NOLINT
fd_set fds; fd_set fds;
FD_ZERO(&fds); // NOLINT FD_ZERO(&fds); // NOLINT
FD_SET(STDIN_FILENO, &fds); // NOLINT FD_SET(STDIN_FILENO, &fds); // NOLINT
@@ -171,24 +121,6 @@ int CheckStdinReady(int usec_timeout) {
return FD_ISSET(STDIN_FILENO, &fds); // NOLINT return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
} }
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = TerminalInputParser(std::move(out));
while (!*quit) {
if (!CheckStdinReady(timeout_microseconds)) {
parser.Timeout(timeout_milliseconds);
continue;
}
const size_t buffer_size = 100;
std::array<char, buffer_size> buffer; // NOLINT;
size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
for (size_t i = 0; i < l; ++i) {
parser.Add(buffer[i]); // NOLINT
}
}
}
#endif #endif
std::stack<Closure> on_exit_functions; // NOLINT std::stack<Closure> on_exit_functions; // NOLINT
@@ -335,34 +267,26 @@ class CapturedMouseImpl : public CapturedMouseInterface {
std::function<void(void)> callback_; std::function<void(void)> callback_;
}; };
void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
// Animation at around 60fps.
const auto time_delta = std::chrono::milliseconds(15);
while (!*quit) {
out->Send(AnimationTask());
std::this_thread::sleep_for(time_delta);
}
}
} // namespace } // namespace
ScreenInteractive::ScreenInteractive(int dimx, ScreenInteractive::ScreenInteractive(Dimension dimension,
int dimx,
int dimy, int dimy,
Dimension dimension,
bool use_alternative_screen) bool use_alternative_screen)
: Screen(dimx, dimy), : Screen(dimx, dimy),
dimension_(dimension), dimension_(dimension),
use_alternative_screen_(use_alternative_screen) { use_alternative_screen_(use_alternative_screen) {
task_receiver_ = MakeReceiver<Task>(); internal_ = std::make_unique<Internal>(
[&](Event event) { PostEvent(std::move(event)); });
} }
// static // static
ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) { ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
return { return {
Dimension::Fixed,
dimx, dimx,
dimy, dimy,
Dimension::Fixed, /*use_alternative_screen=*/false,
false,
}; };
} }
@@ -379,11 +303,12 @@ ScreenInteractive ScreenInteractive::Fullscreen() {
/// content might mess up with the terminal content. /// content might mess up with the terminal content.
// static // static
ScreenInteractive ScreenInteractive::FullscreenPrimaryScreen() { ScreenInteractive ScreenInteractive::FullscreenPrimaryScreen() {
auto terminal = Terminal::Size();
return { return {
0,
0,
Dimension::Fullscreen, Dimension::Fullscreen,
false, terminal.dimx,
terminal.dimy,
/*use_alternative_screen=*/false,
}; };
} }
@@ -391,30 +316,39 @@ ScreenInteractive ScreenInteractive::FullscreenPrimaryScreen() {
/// alternate screen buffer to avoid messing with the terminal content. /// alternate screen buffer to avoid messing with the terminal content.
// static // static
ScreenInteractive ScreenInteractive::FullscreenAlternateScreen() { ScreenInteractive ScreenInteractive::FullscreenAlternateScreen() {
auto terminal = Terminal::Size();
return { return {
0,
0,
Dimension::Fullscreen, Dimension::Fullscreen,
true, terminal.dimx,
terminal.dimy,
/*use_alternative_screen=*/true,
}; };
} }
/// Create a ScreenInteractive whose width match the terminal output width and
/// the height matches the component being drawn.
// static // static
ScreenInteractive ScreenInteractive::TerminalOutput() { ScreenInteractive ScreenInteractive::TerminalOutput() {
auto terminal = Terminal::Size();
return { return {
0,
0,
Dimension::TerminalOutput, Dimension::TerminalOutput,
false, terminal.dimx,
terminal.dimy, // Best guess.
/*use_alternative_screen=*/false,
}; };
} }
ScreenInteractive::~ScreenInteractive() = default;
/// Create a ScreenInteractive whose width and height match the component being
/// drawn.
// static // static
ScreenInteractive ScreenInteractive::FitComponent() { ScreenInteractive ScreenInteractive::FitComponent() {
auto terminal = Terminal::Size();
return { return {
0,
0,
Dimension::FitComponent, Dimension::FitComponent,
terminal.dimx, // Best guess.
terminal.dimy, // Best guess.
false, false,
}; };
} }
@@ -438,16 +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 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).
void ScreenInteractive::HandlePipedInput(bool enable) {
handle_piped_input_ = enable;
}
/// @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) {
// Task/Events sent toward inactive screen or screen waiting to become internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
// inactive are dropped. HandleTask(component_, task);
if (!task_sender_) { });
return;
}
task_sender_->Send(std::move(task));
} }
/// @brief Add an event to the main loop. /// @brief Add an event to the main loop.
@@ -491,7 +433,7 @@ void ScreenInteractive::Loop(Component component) { // NOLINT
/// @brief Return whether the main loop has been quit. /// @brief Return whether the main loop has been quit.
bool ScreenInteractive::HasQuitted() { bool ScreenInteractive::HasQuitted() {
return task_receiver_->HasQuitted(); return quit_;
} }
// private // private
@@ -648,7 +590,15 @@ void ScreenInteractive::Install() {
SetConsoleMode(stdin_handle, in_mode); SetConsoleMode(stdin_handle, in_mode);
SetConsoleMode(stdout_handle, out_mode); SetConsoleMode(stdout_handle, out_mode);
#else #else // POSIX (Linux & Mac)
// #if defined(__EMSCRIPTEN__)
//// Reading stdin isn't blocking.
// int flags = fcntl(0, F_GETFL, 0);
// fcntl(0, F_SETFL, flags | O_NONBLOCK);
//// Restore the terminal configuration on exit.
// on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
// #endif
for (const int signal : {SIGWINCH, SIGTSTP}) { for (const int signal : {SIGWINCH, SIGTSTP}) {
InstallSignalHandler(signal); InstallSignalHandler(signal);
} }
@@ -720,41 +670,102 @@ void ScreenInteractive::Install() {
// ensure it is fully applied: // ensure it is fully applied:
Flush(); 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; quit_ = false;
task_sender_ = task_receiver_->MakeSender();
event_listener_ = PostAnimationTask();
std::thread(&EventListener, &quit_, task_receiver_->MakeSender()); }
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender()); 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 // private
void ScreenInteractive::Uninstall() { void ScreenInteractive::Uninstall() {
ExitNow(); ExitNow();
event_listener_.join();
animation_listener_.join();
OnExit(); OnExit();
} }
// private // private
// NOLINTNEXTLINE // NOLINTNEXTLINE
void ScreenInteractive::RunOnceBlocking(Component component) { void ScreenInteractive::RunOnceBlocking(Component component) {
ExecuteSignalHandlers(); // Set FPS to 60 at most.
Task task; const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
if (task_receiver_->Receive(&task)) {
HandleTask(component, task); auto time = std::chrono::steady_clock::now();
} size_t executed_task = internal_->task_runner.ExecutedTasks();
// Wait for at least one task to execute.
while (executed_task == internal_->task_runner.ExecutedTasks() &&
!HasQuitted()) {
RunOnce(component); RunOnce(component);
const auto now = std::chrono::steady_clock::now();
const auto delta = now - time;
time = now;
if (delta < time_per_frame) {
const auto sleep_duration = time_per_frame - delta;
std::this_thread::sleep_for(sleep_duration);
}
}
} }
// private // private
void ScreenInteractive::RunOnce(Component component) { void ScreenInteractive::RunOnce(Component component) {
Task task; AutoReset set_component(&component_, component);
while (task_receiver_->ReceiveNonBlocking(&task)) {
HandleTask(component, task);
ExecuteSignalHandlers(); ExecuteSignalHandlers();
FetchTerminalEvents();
// Execute the pending tasks from the queue.
const size_t executed_task = internal_->task_runner.ExecutedTasks();
internal_->task_runner.RunUntilIdle();
// If no executed task, we can return early without redrawing the screen.
if (executed_task == internal_->task_runner.ExecutedTasks()) {
return;
} }
Draw(std::move(component));
ExecuteSignalHandlers();
Draw(component);
if (selection_data_previous_ != selection_data_) { if (selection_data_previous_ != selection_data_) {
selection_data_previous_ = selection_data_; selection_data_previous_ = selection_data_;
@@ -775,6 +786,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
// clang-format off // clang-format off
// Handle Event. // Handle Event.
if constexpr (std::is_same_v<T, Event>) { if constexpr (std::is_same_v<T, Event>) {
if (arg.is_cursor_position()) { if (arg.is_cursor_position()) {
cursor_x_ = arg.cursor_x(); cursor_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y(); cursor_y_ = arg.cursor_y();
@@ -921,7 +933,7 @@ void ScreenInteractive::Draw(Component component) {
break; break;
} }
const bool resized = (dimx != dimx_) || (dimy != dimy_); const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
ResetCursorPosition(); ResetCursorPosition();
std::cout << ResetPosition(/*clear=*/resized); std::cout << ResetPosition(/*clear=*/resized);
@@ -1004,6 +1016,7 @@ void ScreenInteractive::Draw(Component component) {
Flush(); Flush();
Clear(); Clear();
frame_valid_ = true; frame_valid_ = true;
frame_count_++;
} }
// private // private
@@ -1025,7 +1038,6 @@ void ScreenInteractive::Exit() {
// private: // private:
void ScreenInteractive::ExitNow() { void ScreenInteractive::ExitNow() {
quit_ = true; quit_ = true;
task_sender_.reset();
} }
// private: // private:
@@ -1058,6 +1070,118 @@ void ScreenInteractive::Signal(int signal) {
#endif #endif
} }
void ScreenInteractive::FetchTerminalEvents() {
#if defined(_WIN32)
auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
// Check if there is input in the console.
auto console = GetStdHandle(STD_INPUT_HANDLE);
DWORD number_of_events = 0;
if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
return std::vector<INPUT_RECORD>();
}
if (number_of_events <= 0) {
// No input, return.
return std::vector<INPUT_RECORD>();
}
// Read the input events.
std::vector<INPUT_RECORD> records(number_of_events);
DWORD number_of_events_read = 0;
if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
&number_of_events_read)) {
return std::vector<INPUT_RECORD>();
}
records.resize(number_of_events_read);
return records;
};
auto records = get_input_records();
if (records.size() == 0) {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
const size_t timeout_microseconds =
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
internal_->terminal_input_parser.Timeout(timeout_microseconds);
return;
}
internal_->last_char_time = std::chrono::steady_clock::now();
// Convert the input events to FTXUI events.
// For each event, we call the terminal input parser to convert it to
// Event.
for (const auto& r : records) {
switch (r.EventType) {
case KEY_EVENT: {
auto key_event = r.Event.KeyEvent;
// ignore UP key events
if (key_event.bKeyDown == FALSE) {
continue;
}
std::wstring wstring;
wstring += key_event.uChar.UnicodeChar;
for (auto it : to_string(wstring)) {
internal_->terminal_input_parser.Add(it);
}
} break;
case WINDOW_BUFFER_SIZE_EVENT:
Post(Event::Special({0}));
break;
case MENU_EVENT:
case FOCUS_EVENT:
case MOUSE_EVENT:
// TODO(mauve): Implement later.
break;
}
}
#elif defined(__EMSCRIPTEN__)
// Read chars from the terminal.
// We configured it to be non blocking.
std::array<char, 128> out{};
size_t l = read(STDIN_FILENO, out.data(), out.size());
if (l == 0) {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
const size_t timeout_microseconds =
std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
internal_->terminal_input_parser.Timeout(timeout_microseconds);
return;
}
internal_->last_char_time = std::chrono::steady_clock::now();
// Convert the chars to events.
for (size_t i = 0; i < l; ++i) {
internal_->terminal_input_parser.Add(out[i]);
}
#else // POSIX (Linux & Mac)
if (!CheckStdinReady()) {
const auto timeout =
std::chrono::steady_clock::now() - internal_->last_char_time;
const size_t timeout_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
internal_->terminal_input_parser.Timeout(timeout_ms);
return;
}
internal_->last_char_time = std::chrono::steady_clock::now();
// Read chars from the terminal.
std::array<char, 128> out{};
size_t l = read(fileno(stdin), out.data(), out.size());
// Convert the chars to events.
for (size_t i = 0; i < l; ++i) {
internal_->terminal_input_parser.Add(out[i]);
}
#endif
}
void ScreenInteractive::PostAnimationTask() {
Post(AnimationTask());
// Repeat the animation task every 15ms. This correspond to a frame rate
// of around 66fps.
internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
std::chrono::milliseconds(15));
}
bool ScreenInteractive::SelectionData::operator==( bool ScreenInteractive::SelectionData::operator==(
const ScreenInteractive::SelectionData& other) const { const ScreenInteractive::SelectionData& other) const {
if (empty && other.empty) { if (empty && other.empty) {

View File

@@ -0,0 +1,220 @@
// 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, 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 redirect stdin since HandlePipedInput is on by default
screen.Install();
// Stdin should be the tty
EXPECT_TRUE(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, ExplicitlyEnabled) {
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();
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();
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();
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__)

View File

@@ -10,9 +10,58 @@
#include "ftxui/component/screen_interactive.hpp" #include "ftxui/component/screen_interactive.hpp"
#include "ftxui/dom/elements.hpp" // for text, Element #include "ftxui/dom/elements.hpp" // for text, Element
#if defined(__unix__)
#include <fcntl.h>
#include <unistd.h>
#include <array>
#include <cstdio>
#include <ftxui/component/loop.hpp>
#include <string>
#endif
namespace ftxui { namespace ftxui {
namespace { namespace {
#if defined(__unix__)
// Capture the standard output (stdout) to a string.
class StdCapture {
public:
explicit StdCapture(std::string* captured) : captured_(captured) {
if (pipe(pipefd_) != 0) {
return;
}
old_stdout_ = dup(fileno(stdout));
fflush(stdout);
dup2(pipefd_[1], fileno(stdout));
close(pipefd_[1]); // Close the write end in the parent
}
~StdCapture() {
fflush(stdout);
dup2(old_stdout_, fileno(stdout));
close(old_stdout_);
char buffer[1024];
ssize_t count;
while ((count = read(pipefd_[0], buffer, sizeof(buffer))) > 0) {
captured_->append(buffer, count);
}
close(pipefd_[0]);
}
StdCapture(const StdCapture&) = delete;
StdCapture& operator=(const StdCapture&) = delete;
private:
int pipefd_[2]{-1, -1};
int old_stdout_{-1};
std::string* const captured_;
};
#endif
bool TestSignal(int signal) { bool TestSignal(int signal) {
int called = 0; int called = 0;
// The tree of components. This defines how to navigate using the keyboard. // The tree of components. This defines how to navigate using the keyboard.
@@ -131,4 +180,66 @@ TEST(ScreenInteractive, CtrlC_NotForced) {
ASSERT_GE(ctrl_c_count, 50); ASSERT_GE(ctrl_c_count, 50);
} }
// Regression test for:
// https://github.com/ArthurSonzogni/FTXUI/pull/1064/files
TEST(ScreenInteractive, FixedSizeInitialFrame) {
#if defined(__unix__)
std::string output;
{
auto capture = StdCapture(&output);
auto screen = ScreenInteractive::FixedSize(2, 2);
auto component = Renderer([&] { return text("AB"); });
Loop loop(&screen, component);
loop.RunOnce();
}
using namespace std::string_view_literals;
auto expected =
// Install the ScreenInteractive.
"\0" // Flush stdout.
"\x1BP$q q" // Set cursor shape to 1 (block).
"\x1B\\" // Reset cursor position.
"\x1B[?7l" // Disable line wrapping.
"\x1B[?1000h" // Enable mouse tracking.
"\x1B[?1003h" // Enable mouse motion tracking.
"\x1B[?1015h" // Enable mouse wheel tracking.
"\x1B[?1006h" // Enable SGR mouse tracking.
"\0" // Flush stdout.
// Reset the screen.
"\r" // Reset cursor position.
"\x1B[2K" // Clear the line.
"\x1B[1A" // Move cursor up one line.
"\x1B[2K" // Clear the line.
// Print the document.
"AB\r\n" // Print "AB" and move to the next line.
" " // Print two spaces to fill the line.
// Set cursor position.
"\x1B[1D" // Move cursor left one character.
"\x1B[?25l" // Hide cursor.
// Flush
"\0" // Flush stdout.
// Uninstall the ScreenInteractive.
"\x1B[1C" // Move cursor right one character.
"\x1B[?1006l" // Disable SGR mouse tracking.
"\x1B[?1015l" // Disable mouse wheel tracking.
"\x1B[?1003l" // Disable mouse motion tracking.
"\x1B[?1000l" // Disable mouse tracking.
"\x1B[?7h" // Enable line wrapping.
"\x1B[?25h" // Show cursor.
"\x1B[1 q" // Set cursor shape to 1 (block).
"\0" // Flush stdout.
// Skip one line to avoid the prompt to be printed over the last drawing.
"\r\n"sv;
ASSERT_EQ(expected, output);
#endif
}
} // namespace ftxui } // namespace ftxui

View File

@@ -0,0 +1,19 @@
// Copyright 2024 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 "ftxui/component/task_internal.hpp"
namespace ftxui::task {
bool PendingTask::operator<(const PendingTask& other) const {
if (!time && !other.time) {
return false;
}
if (!time) {
return true;
}
if (!other.time) {
return false;
}
return time.value() > other.time.value();
}
} // namespace ftxui::task

View File

@@ -0,0 +1,40 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_HPP
#define TASK_HPP
#include <chrono>
#include <functional>
#include <optional>
namespace ftxui::task {
/// A task represents a unit of work.
using Task = std::function<void()>;
/// A PendingTask represents a task that is scheduled to be executed at a
/// specific time, or as soon as possible.
struct PendingTask {
// Immediate task:
PendingTask(Task t) : task(std::move(t)) {} // NOLINT
// Delayed task with a duration
PendingTask(Task t, std::chrono::steady_clock::duration duration)
: task(std::move(t)), time(std::chrono::steady_clock::now() + duration) {}
/// The task to be executed.
Task task;
/// The time when the task should be executed. If the time is empty, the task
/// should be executed as soon as possible.
std::optional<std::chrono::steady_clock::time_point> time;
/// Compare two PendingTasks by their time.
/// If both tasks have no time, they are considered equal.
bool operator<(const PendingTask& other) const;
};
} // namespace ftxui::task
#endif // TASK_HPP_

View File

@@ -0,0 +1,53 @@
// Copyright 2024 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 "ftxui/component/task_queue.hpp"
namespace ftxui::task {
auto TaskQueue::PostTask(PendingTask task) -> void {
if (!task.time) {
immediate_tasks_.push(task);
return;
}
if (task.time.value() < std::chrono::steady_clock::now()) {
immediate_tasks_.push(task);
return;
}
delayed_tasks_.push(task);
}
auto TaskQueue::Get() -> MaybeTask {
// Attempt to execute a task immediately.
if (!immediate_tasks_.empty()) {
auto task = immediate_tasks_.front();
immediate_tasks_.pop();
return task.task;
}
// Move all tasks that can be executed to the immediate queue.
auto now = std::chrono::steady_clock::now();
while (!delayed_tasks_.empty() && delayed_tasks_.top().time.value() <= now) {
immediate_tasks_.push(delayed_tasks_.top());
delayed_tasks_.pop();
}
// Attempt to execute a task immediately.
if (!immediate_tasks_.empty()) {
auto task = immediate_tasks_.front();
immediate_tasks_.pop();
return task.task;
}
// If there are no tasks to execute, return the delay until the next task.
if (!delayed_tasks_.empty()) {
return delayed_tasks_.top().time.value() - now;
}
// If there are no tasks to execute, return the maximum duration.
return std::monostate{};
}
} // namespace ftxui::task

View File

@@ -0,0 +1,37 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_QUEUE_HPP
#define TASK_QUEUE_HPP
#include <queue>
#include <variant>
#include "ftxui/component/task_internal.hpp" // for PendingTask, Task
namespace ftxui::task {
/// A task queue that schedules tasks to be executed in the future. Tasks can be
/// scheduled to be executed immediately, or after a certain duration.
/// - The tasks are executed in the order they were scheduled.
/// - If multiple tasks are scheduled to be executed at the same time, they are
/// executed in the order they were scheduled.
/// - If a task is scheduled to be executed in the past, it is executed
/// immediately.
struct TaskQueue {
auto PostTask(PendingTask task) -> void;
using MaybeTask =
std::variant<Task, std::chrono::steady_clock::duration, std::monostate>;
auto Get() -> MaybeTask;
bool HasImmediateTasks() const { return !immediate_tasks_.empty(); }
private:
std::queue<PendingTask> immediate_tasks_;
std::priority_queue<PendingTask> delayed_tasks_;
};
} // namespace ftxui::task
#endif

View File

@@ -0,0 +1,75 @@
// Copyright 2024 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 "ftxui/component/task_runner.hpp"
#include <cassert>
#include <thread>
namespace ftxui::task {
static thread_local TaskRunner* current_task_runner = nullptr; // NOLINT
TaskRunner::TaskRunner() {
assert(!previous_task_runner_);
previous_task_runner_ = current_task_runner;
current_task_runner = this;
}
TaskRunner::~TaskRunner() {
current_task_runner = previous_task_runner_;
}
// static
auto TaskRunner::Current() -> TaskRunner* {
assert(current_task_runner);
return current_task_runner;
}
auto TaskRunner::PostTask(Task task) -> void {
queue_.PostTask(PendingTask{std::move(task)});
}
auto TaskRunner::PostDelayedTask(Task task,
std::chrono::steady_clock::duration duration)
-> void {
queue_.PostTask(PendingTask{std::move(task), duration});
}
/// Runs the tasks in the queue.
auto TaskRunner::RunUntilIdle()
-> std::optional<std::chrono::steady_clock::duration> {
while (true) {
auto maybe_task = queue_.Get();
if (std::holds_alternative<std::monostate>(maybe_task)) {
// No more tasks to execute, exit the loop.
return std::nullopt;
}
if (std::holds_alternative<Task>(maybe_task)) {
executed_tasks_++;
std::get<Task>(maybe_task)();
continue;
}
if (std::holds_alternative<std::chrono::steady_clock::duration>(
maybe_task)) {
return std::get<std::chrono::steady_clock::duration>(maybe_task);
}
}
}
auto TaskRunner::Run() -> void {
while (true) {
auto duration = RunUntilIdle();
if (!duration) {
// No more tasks to execute, exit the loop.
return;
}
// Sleep for the duration until the next task can be executed.
std::this_thread::sleep_for(duration.value());
}
}
} // namespace ftxui::task

View File

@@ -0,0 +1,46 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_RUNNER_HPP
#define TASK_RUNNER_HPP
#include "ftxui/component/task_internal.hpp"
#include "ftxui/component/task_queue.hpp"
namespace ftxui::task {
class TaskRunner {
public:
TaskRunner();
~TaskRunner();
// Returns the task runner for the current thread.
static auto Current() -> TaskRunner*;
/// Schedules a task to be executed immediately.
auto PostTask(Task task) -> void;
/// Schedules a task to be executed after a certain duration.
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration)
-> void;
/// Runs the tasks in the queue, return the delay until the next delayed task
/// can be executed.
auto RunUntilIdle() -> std::optional<std::chrono::steady_clock::duration>;
// Runs the tasks in the queue, blocking until all tasks are executed.
auto Run() -> void;
bool HasImmediateTasks() const { return queue_.HasImmediateTasks(); }
size_t ExecutedTasks() const { return executed_tasks_; }
private:
TaskRunner* previous_task_runner_ = nullptr;
TaskQueue queue_;
size_t executed_tasks_ = 0;
};
} // namespace ftxui::task
#endif // TASK_RUNNER_HPP

View File

@@ -0,0 +1,94 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
// Copyright 2024 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 "ftxui/component/task_internal.hpp"
#include <gtest/gtest.h>
#include <thread> // for sleep_for
#include "ftxui/component/task_runner.hpp"
namespace ftxui::task {
TEST(TaskTest, Basic) {
std::vector<int> values;
auto task_1 = [&values] { values.push_back(1); };
auto task_2 = [&values] { values.push_back(2); };
auto task_3 = [&values] { values.push_back(3); };
auto runner = TaskRunner();
runner.PostTask(task_1);
runner.PostTask(task_2);
runner.PostTask(task_3);
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3}));
}
TEST(TaskTest, PostedWithinTask) {
std::vector<int> values;
auto task_1 = [&values] {
values.push_back(1);
auto task_2 = [&values] { values.push_back(5); };
TaskRunner::Current()->PostTask(std::move(task_2));
values.push_back(2);
};
auto task_2 = [&values] {
values.push_back(3);
auto task_2 = [&values] { values.push_back(6); };
TaskRunner::Current()->PostTask(std::move(task_2));
values.push_back(4);
};
auto runner = TaskRunner();
runner.PostTask(task_1);
runner.PostTask(task_2);
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3, 4, 5, 6}));
}
TEST(TaskTest, RunDelayedTask) {
std::vector<int> values;
auto task_1 = [&values] { values.push_back(1); };
auto task_2 = [&values] { values.push_back(2); };
auto task_3 = [&values] { values.push_back(3); };
auto runner = TaskRunner();
runner.PostDelayedTask(task_3, std::chrono::milliseconds(300));
runner.PostDelayedTask(task_1, std::chrono::milliseconds(100));
runner.PostDelayedTask(task_2, std::chrono::milliseconds(200));
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3}));
}
} // namespace ftxui::task

View File

@@ -5,7 +5,7 @@
#include <cstdint> // for uint32_t #include <cstdint> // for uint32_t
#include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion #include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion
#include <ftxui/component/receiver.hpp> // for SenderImpl, Sender #include <functional> // for std::function
#include <map> #include <map>
#include <memory> // for unique_ptr, allocator #include <memory> // for unique_ptr, allocator
#include <utility> // for move #include <utility> // for move
@@ -90,7 +90,7 @@ const std::map<std::string, std::string> g_uniformize = {
{"\x1B[X", "\x1B[24~"}, // F12 {"\x1B[X", "\x1B[24~"}, // F12
}; };
TerminalInputParser::TerminalInputParser(Sender<Task> out) TerminalInputParser::TerminalInputParser(std::function<void(Event)> out)
: out_(std::move(out)) {} : out_(std::move(out)) {}
void TerminalInputParser::Timeout(int time) { void TerminalInputParser::Timeout(int time) {
@@ -131,7 +131,7 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
return; return;
case CHARACTER: case CHARACTER:
out_->Send(Event::Character(std::move(pending_))); out_(Event::Character(std::move(pending_)));
pending_.clear(); pending_.clear();
return; return;
@@ -140,25 +140,25 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
if (it != g_uniformize.end()) { if (it != g_uniformize.end()) {
pending_ = it->second; pending_ = it->second;
} }
out_->Send(Event::Special(std::move(pending_))); out_(Event::Special(std::move(pending_)));
pending_.clear(); pending_.clear();
} }
return; return;
case MOUSE: case MOUSE:
out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
pending_.clear(); pending_.clear();
return; return;
case CURSOR_POSITION: case CURSOR_POSITION:
out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT out_(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT output.cursor.y)); // NOLINT
pending_.clear(); pending_.clear();
return; return;
case CURSOR_SHAPE: case CURSOR_SHAPE:
out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape)); out_(Event::CursorShape(std::move(pending_), output.cursor_shape));
pending_.clear(); pending_.clear();
return; return;
} }

View File

@@ -4,12 +4,11 @@
#ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER #ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER #define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#include <functional>
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "ftxui/component/mouse.hpp" // for Mouse #include "ftxui/component/mouse.hpp" // for Mouse
#include "ftxui/component/receiver.hpp" // for Sender
#include "ftxui/component/task.hpp" // for Task
namespace ftxui { namespace ftxui {
struct Event; struct Event;
@@ -17,7 +16,7 @@ struct Event;
// Parse a sequence of |char| accross |time|. Produces |Event|. // Parse a sequence of |char| accross |time|. Produces |Event|.
class TerminalInputParser { class TerminalInputParser {
public: public:
explicit TerminalInputParser(Sender<Task> out); explicit TerminalInputParser(std::function<void(Event)> out);
void Timeout(int time); void Timeout(int time);
void Add(char c); void Add(char c);
@@ -62,7 +61,7 @@ class TerminalInputParser {
Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments); Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments);
Output ParseCursorPosition(std::vector<int> arguments); Output ParseCursorPosition(std::vector<int> arguments);
Sender<Task> out_; std::function<void(Event)> out_;
int position_ = -1; int position_ = -1;
int timeout_ = 0; int timeout_ = 0;
std::string pending_; std::string pending_;

View File

@@ -2,12 +2,12 @@
// 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 <ftxui/component/mouse.hpp> // for Mouse, Mouse::Left, Mouse::Middle, Mouse::Pressed, Mouse::Released, Mouse::Right #include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Left, Mouse::Middle, Mouse::Pressed, Mouse::Released, Mouse::Right
#include <ftxui/component/task.hpp> // for Task #include <functional> // for function
#include <initializer_list> // for initializer_list #include <initializer_list> // for initializer_list
#include <memory> // for allocator, unique_ptr #include <memory> // for allocator, unique_ptr
#include <vector> // for vector
#include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::End, Event::Home, Event::Custom, Event::Delete, Event::F1, Event::F10, Event::F11, Event::F12, Event::F2, Event::F3, Event::F4, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape #include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::End, Event::Home, Event::Custom, Event::Delete, Event::F1, Event::F10, Event::F11, Event::F12, Event::F2, Event::F3, Event::F4, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape
#include "ftxui/component/receiver.hpp" // for MakeReceiver, ReceiverImpl
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
#include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE #include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE
@@ -17,87 +17,77 @@ namespace ftxui {
// Test char |c| to are trivially converted into |Event::Character(c)|. // Test char |c| to are trivially converted into |Event::Character(c)|.
TEST(Event, Character) { TEST(Event, Character) {
std::vector<char> basic_char; std::vector<char> basic_char;
for (char c = 'a'; c <= 'z'; ++c) for (char c = 'a'; c <= 'z'; ++c) {
basic_char.push_back(c); basic_char.push_back(c);
for (char c = 'A'; c <= 'Z'; ++c) }
for (char c = 'A'; c <= 'Z'; ++c) {
basic_char.push_back(c); basic_char.push_back(c);
}
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
for (char c : basic_char) for (char c : basic_char) {
parser.Add(c); parser.Add(c);
} }
Task received; for (size_t i = 0; i < basic_char.size(); ++i) {
for (char c : basic_char) { EXPECT_TRUE(received_events[i].is_character());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_EQ(basic_char[i], received_events[i].character()[0]);
EXPECT_TRUE(std::get<Event>(received).is_character());
EXPECT_EQ(c, std::get<Event>(received).character()[0]);
} }
EXPECT_FALSE(event_receiver->Receive(&received)); EXPECT_EQ(received_events.size(), basic_char.size());
} }
TEST(Event, EscapeKeyWithoutWaiting) { TEST(Event, EscapeKeyWithoutWaiting) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
}
Task received; EXPECT_TRUE(received_events.empty());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeKeyNotEnoughWait) { TEST(Event, EscapeKeyNotEnoughWait) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Timeout(49); parser.Timeout(49);
}
Task received; EXPECT_TRUE(received_events.empty());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeKeyEnoughWait) { TEST(Event, EscapeKeyEnoughWait) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Timeout(50); parser.Timeout(50);
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_EQ(received_events[0], Event::Escape);
EXPECT_EQ(std::get<Event>(received), Event::Escape);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeFast) { TEST(Event, EscapeFast) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('a'); parser.Add('a');
parser.Add('\x1B'); parser.Add('');
parser.Add('b'); parser.Add('b');
parser.Timeout(49); parser.Timeout(49);
}
Task received; EXPECT_EQ(2, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_EQ(received_events[0], Event::AltA);
EXPECT_EQ(std::get<Event>(received), Event::AltA); EXPECT_EQ(received_events[1], Event::AltB);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::AltB);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftClickPressed) { TEST(Event, MouseLeftClickPressed) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('0'); parser.Add('0');
parser.Add(';'); parser.Add(';');
@@ -107,23 +97,20 @@ TEST(Event, MouseLeftClickPressed) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_TRUE(std::get<Event>(received).is_mouse()); EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button); EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(12, std::get<Event>(received).mouse().x); EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(42, std::get<Event>(received).mouse().y); EXPECT_EQ(received_events[0].mouse().motion, Mouse::Pressed);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Pressed);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftMoved) { TEST(Event, MouseLeftMoved) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('2'); parser.Add('2');
@@ -134,23 +121,20 @@ TEST(Event, MouseLeftMoved) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_TRUE(std::get<Event>(received).is_mouse()); EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button); EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(12, std::get<Event>(received).mouse().x); EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(42, std::get<Event>(received).mouse().y); EXPECT_EQ(received_events[0].mouse().motion, Mouse::Moved);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Moved);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftClickReleased) { TEST(Event, MouseLeftClickReleased) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('0'); parser.Add('0');
parser.Add(';'); parser.Add(';');
@@ -160,23 +144,20 @@ TEST(Event, MouseLeftClickReleased) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('m'); parser.Add('m');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_TRUE(std::get<Event>(received).is_mouse()); EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button); EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(12, std::get<Event>(received).mouse().x); EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(42, std::get<Event>(received).mouse().y); EXPECT_EQ(received_events[0].mouse().motion, Mouse::Released);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Released);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseReporting) { TEST(Event, MouseReporting) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('1'); parser.Add('1');
parser.Add('2'); parser.Add('2');
@@ -184,21 +165,18 @@ TEST(Event, MouseReporting) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('R'); parser.Add('R');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_cursor_position());
EXPECT_TRUE(std::get<Event>(received).is_cursor_position()); EXPECT_EQ(42, received_events[0].cursor_x());
EXPECT_EQ(42, std::get<Event>(received).cursor_x()); EXPECT_EQ(12, received_events[0].cursor_y());
EXPECT_EQ(12, std::get<Event>(received).cursor_y());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseMiddleClick) { TEST(Event, MouseMiddleClick) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('3'); parser.Add('3');
@@ -209,22 +187,19 @@ TEST(Event, MouseMiddleClick) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_TRUE(std::get<Event>(received).is_mouse()); EXPECT_EQ(Mouse::Middle, received_events[0].mouse().button);
EXPECT_EQ(Mouse::Middle, std::get<Event>(received).mouse().button); EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(12, std::get<Event>(received).mouse().x); EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseRightClick) { TEST(Event, MouseRightClick) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('\x1B'); parser.Add('');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('4'); parser.Add('4');
@@ -235,15 +210,12 @@ TEST(Event, MouseRightClick) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_TRUE(std::get<Event>(received).is_mouse()); EXPECT_EQ(Mouse::Right, received_events[0].mouse().button);
EXPECT_EQ(Mouse::Right, std::get<Event>(received).mouse().button); EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(12, std::get<Event>(received).mouse().x); EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, UTF8) { TEST(Event, UTF8) {
@@ -313,31 +285,30 @@ TEST(Event, UTF8) {
}; };
for (auto test : kTestCase) { for (auto test : kTestCase) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
for (auto input : test.input) for (auto input : test.input) {
parser.Add(input); parser.Add(input);
} }
Task received;
if (test.valid) { if (test.valid) {
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(std::get<Event>(received).is_character()); EXPECT_TRUE(received_events[0].is_character());
} else {
EXPECT_TRUE(received_events.empty());
} }
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} }
TEST(Event, NewLine) { TEST(Event, NewLine) {
for (char newline : {'\r', '\n'}) { for (char newline : {'\r', '\n'}) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(newline); parser.Add(newline);
} EXPECT_EQ(1, received_events.size());
Task received; EXPECT_TRUE(received_events[0] == Event::Return);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received) == Event::Return);
} }
} }
@@ -348,8 +319,9 @@ TEST(Event, Control) {
}; };
std::vector<TestCase> cases; std::vector<TestCase> cases;
for (int i = 0; i < 32; ++i) { for (int i = 0; i < 32; ++i) {
if (i == 8 || i == 13 || i == 24 || i == 26 || i == 27) if (i == 8 || i == 13 || i == 24 || i == 26 || i == 27) {
continue; continue;
}
cases.push_back({char(i), false}); cases.push_back({char(i), false});
} }
cases.push_back({char(24), false}); cases.push_back({char(24), false});
@@ -357,17 +329,16 @@ TEST(Event, Control) {
cases.push_back({char(127), false}); cases.push_back({char(127), false});
for (auto test : cases) { for (auto test : cases) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(test.input); parser.Add(test.input);
}
Task received;
if (test.cancel) { if (test.cancel) {
EXPECT_FALSE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events.empty());
} else { } else {
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_EQ(1, received_events.size());
EXPECT_EQ(std::get<Event>(received), Event::Special({test.input})); EXPECT_EQ(received_events[0], Event::Special({test.input}));
} }
} }
} }
@@ -375,8 +346,9 @@ TEST(Event, Control) {
TEST(Event, Special) { TEST(Event, Special) {
auto str = [](std::string input) { auto str = [](std::string input) {
std::vector<unsigned char> output; std::vector<unsigned char> output;
for (auto it : input) for (auto it : input) {
output.push_back(it); output.push_back(it);
}
return output; return output;
}; };
@@ -385,10 +357,12 @@ TEST(Event, Special) {
Event expected; Event expected;
} kTestCase[] = { } kTestCase[] = {
// Arrow (default cursor mode) // Arrow (default cursor mode)
{str("\x1B[A"), Event::ArrowUp}, {str("\x1B[B"), Event::ArrowDown}, {str(""), Event::ArrowUp},
{str("\x1B[C"), Event::ArrowRight}, {str("\x1B[D"), Event::ArrowLeft}, {str(""), Event::ArrowDown},
{str("\x1B[H"), Event::Home}, {str("\x1B[F"), Event::End}, {str(""), Event::ArrowRight},
/* {str(""), Event::ArrowLeft},
{str(""), Event::Home},
{str(""), Event::End},
// Arrow (application cursor mode) // Arrow (application cursor mode)
{str("\x1BOA"), Event::ArrowUp}, {str("\x1BOA"), Event::ArrowUp},
@@ -469,28 +443,24 @@ TEST(Event, Special) {
// Custom: // Custom:
{{0}, Event::Custom}, {{0}, Event::Custom},
*/
}; };
for (auto test : kTestCase) { for (auto test : kTestCase) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
for (auto input : test.input) { for (auto input : test.input) {
parser.Add(input); parser.Add(input);
} }
} EXPECT_EQ(1, received_events.size());
Task received; EXPECT_EQ(received_events[0], test.expected);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), test.expected);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} }
TEST(Event, DeviceControlString) { TEST(Event, DeviceControlString) {
auto event_receiver = MakeReceiver<Task>(); std::vector<Event> received_events;
{ auto parser = TerminalInputParser(
auto parser = TerminalInputParser(event_receiver->MakeSender()); [&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(27); // ESC parser.Add(27); // ESC
parser.Add(80); // P parser.Add(80); // P
parser.Add(49); // 1 parser.Add(49); // 1
@@ -501,13 +471,10 @@ TEST(Event, DeviceControlString) {
parser.Add(113); // q parser.Add(113); // q
parser.Add(27); // ESC parser.Add(27); // ESC
parser.Add(92); // (backslash) parser.Add(92); // (backslash)
}
Task received; EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(event_receiver->Receive(&received)); EXPECT_TRUE(received_events[0].is_cursor_shape());
EXPECT_TRUE(std::get<Event>(received).is_cursor_shape()); EXPECT_EQ(1, received_events[0].cursor_shape());
EXPECT_EQ(1, std::get<Event>(received).cursor_shape());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} // namespace ftxui } // namespace ftxui

View File

@@ -1,21 +1,16 @@
// Copyright 2021 Arthur Sonzogni. All rights reserved. // Copyright 2021 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 <cstddef>
#include <ftxui/component/event.hpp>
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) { extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
using namespace ftxui; using namespace ftxui;
auto event_receiver = MakeReceiver<Task>(); auto parser = TerminalInputParser([&](Event) {});
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (size_t i = 0; i < size; ++i) { for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]); parser.Add(data[i]);
} }
}
Task received;
while (event_receiver->Receive(&received)) {
// Do nothing.
}
return 0; // Non-zero return values are reserved for future use. return 0; // Non-zero return values are reserved for future use.
} }

View File

@@ -47,8 +47,9 @@ namespace {
#if defined(_WIN32) #if defined(_WIN32)
void WindowsEmulateVT100Terminal() { void WindowsEmulateVT100Terminal() {
static bool done = false; static bool done = false;
if (done) if (done) {
return; return;
}
done = true; done = true;
// Enable VT processing on stdout and stdin // Enable VT processing on stdout and stdin

View File

@@ -1284,8 +1284,9 @@ bool IsCombining(uint32_t ucs) {
} }
bool IsFullWidth(uint32_t ucs) { bool IsFullWidth(uint32_t ucs) {
if (ucs < 0x0300) // Quick path: // NOLINT if (ucs < 0x0300) { // Quick path: // NOLINT
return false; return false;
}
return Bisearch(ucs, g_full_width_characters); return Bisearch(ucs, g_full_width_characters);
} }

View File

@@ -1,4 +1,4 @@
// Copyright 2024 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.