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 1428 additions and 638 deletions

View File

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

View File

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

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ out/
!doc/**/*.html
!doc/**/*.xml
!doc/**/*.md
!doc/*.md
# examples directory:
!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", "generate_examples")
load(":bazel/ftxui.bzl", "windows_copts")
load(":bazel/ftxui.bzl", "pthread_linkopts")
# 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
@@ -159,11 +158,18 @@ ftxui_cc_library(
"src/ftxui/component/resizable_split.cpp",
"src/ftxui/component/screen_interactive.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.hpp",
"src/ftxui/component/util.cpp",
"src/ftxui/component/window.cpp",
# Private header from ftxui:dom.
"src/ftxui/dom/node_decorator.hpp",
@@ -184,7 +190,6 @@ ftxui_cc_library(
"include/ftxui/component/screen_interactive.hpp",
"include/ftxui/component/task.hpp",
],
linkopts = pthread_linkopts(),
deps = [
":dom",
":screen",
@@ -207,7 +212,6 @@ cc_test(
"src/ftxui/component/menu_test.cpp",
"src/ftxui/component/modal_test.cpp",
"src/ftxui/component/radiobox_test.cpp",
"src/ftxui/component/receiver_test.cpp",
"src/ftxui/component/resizable_split_test.cpp",
"src/ftxui/component/slider_test.cpp",
"src/ftxui/component/terminal_input_parser_test.cpp",

View File

@@ -24,6 +24,7 @@ Next
import ftxui.util;
```
Thanks @mikomikotaishi for PR #1015.
- Remove dependency on 'pthread'.
### Component
- Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal

View File

@@ -144,26 +144,20 @@ add_library(component
src/ftxui/component/resizable_split.cpp
src/ftxui/component/screen_interactive.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.hpp
src/ftxui/component/util.cpp
src/ftxui/component/window.cpp
)
target_link_libraries(dom
PUBLIC screen
)
target_link_libraries(component
PUBLIC dom
)
if (NOT EMSCRIPTEN)
find_package(Threads)
target_link_libraries(component
PUBLIC Threads::Threads
)
endif()
target_link_libraries(dom PUBLIC screen)
target_link_libraries(component PUBLIC dom)
include(cmake/ftxui_set_options.cmake)
ftxui_set_options(screen)

View File

@@ -18,7 +18,7 @@
<br/>
<a href="https://arthursonzogni.github.io/FTXUI/">Documentation</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/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)
- [Consu](https://github.com/cpp-best-practices/game_jam/blob/main/Jam1_April_2022/consu.md)
## 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.

View File

@@ -43,16 +43,6 @@ def windows_copts():
"//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(
name,
srcs = [],

View File

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

View File

@@ -101,6 +101,5 @@ endfunction()
if (EMSCRIPTEN)
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")
endif()

View File

@@ -19,11 +19,10 @@ add_executable(ftxui-tests
src/ftxui/component/menu_test.cpp
src/ftxui/component/modal_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/screen_interactive_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/toggle_test.cpp
src/ftxui/dom/blink_test.cpp
@@ -51,6 +50,7 @@ add_executable(ftxui-tests
src/ftxui/dom/vbox_test.cpp
src/ftxui/screen/color_test.cpp
src/ftxui/screen/string_test.cpp
src/ftxui/util/ref_test.cpp
)
target_link_libraries(ftxui-tests

View File

@@ -12,8 +12,24 @@ FTXUI experimentally supports
compilation times and improve code organization. Each header has a
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
import ftxui;
@@ -26,23 +42,30 @@ int main() {
}
```
```sh
cmake \
-DCMAKE_GENERATOR=Ninja \
-DFTXUI_BUILD_MODULES=ON \
..
Note, the `ftxui` convenience module which simply pulls together all the modules:
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
The modules directly reference the corresponding header, or a group of related
headers to provide a more convenient interface. The following modules
headers to provide a more convenient interface. The following modules
are available:
- `ftxui`

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)
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)
foreach(file
"index.html"
"index.mjs"
"index.css"
"sw.js"
"run_webassembly.py")
configure_file(${file} ${file})
endforeach(file)

View File

@@ -1,9 +1,64 @@
#include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp"
// 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 <memory> // for shared_ptr, __shared_ptr_access
#include <string> // for operator+, to_string
int main(){
auto screen = ftxui::ScreenInteractive::Fullscreen();
auto testComponent = ftxui::Renderer([](){return ftxui::text("test Component");});
screen.Loop(testComponent);
return 0;
#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 value = 50;
// 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;
}

View File

@@ -133,8 +133,9 @@ int main() {
float dy = 50.f;
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]);
}
return canvas(std::move(c));
});

View File

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

View File

@@ -1,11 +1,12 @@
// 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 <stddef.h> // for size_t
#include <array> // for array
#include <atomic> // for atomic
#include <chrono> // for operator""s, chrono_literals
#include <cmath> // for sin
#include <stddef.h> // for size_t
#include <array> // for array
#include <atomic> // for atomic
#include <chrono> // for operator""s, chrono_literals
#include <cmath> // for sin
#include <ftxui/component/loop.hpp>
#include <functional> // for ref, reference_wrapper, function
#include <memory> // for allocator, shared_ptr, __shared_ptr_access
#include <string> // for string, basic_string, char_traits, operator+, to_string
@@ -269,7 +270,7 @@ int main() {
auto spinner_tab_renderer = Renderer([&] {
Elements entries;
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);
}
return hflow(std::move(entries));
@@ -512,24 +513,20 @@ int main() {
});
});
std::atomic<bool> refresh_ui_continue = true;
std::thread refresh_ui([&] {
while (refresh_ui_continue) {
using namespace std::chrono_literals;
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);
}
});
Loop loop(&screen, main_renderer);
while (!loop.HasQuitted()) {
// Update the state of the application.
shift++;
screen.Loop(main_renderer);
refresh_ui_continue = false;
refresh_ui.join();
// Request a new frame to be drawn.
screen.RequestAnimationFrame();
// 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;
}

View File

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

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries;
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));
}
auto radiobox = Menu(&entries, &selected);
auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame |

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries;
int selected = 0;
for (int i = 0; i < 100; ++i)
for (int i = 0; i < 100; ++i) {
entries.push_back(std::to_string(i));
}
auto radiobox = Menu(&entries, &selected, MenuOption::Horizontal());
auto renderer = Renderer(
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) {
state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label);
if (state.focused)
if (state.focused) {
e = e | bgcolor(Color::Blue);
if (state.active)
}
if (state.active) {
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -130,10 +132,12 @@ Component VMenu2(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) {
state.label += (state.active ? " <" : " ");
Element e = hbox(filler(), text(state.label));
if (state.focused)
if (state.focused) {
e = e | bgcolor(Color::Red);
if (state.active)
}
if (state.active) {
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -144,13 +148,16 @@ Component VMenu3(std::vector<std::string>* entries, int* selected) {
option.entries_option.transform = [](EntryState state) {
Element e = state.active ? text("[" + state.label + "]")
: text(" " + state.label + " ");
if (state.focused)
if (state.focused) {
e = e | bold;
}
if (state.focused)
if (state.focused) {
e = e | color(Color::Blue);
if (state.active)
}
if (state.active) {
e = e | bold;
}
return e;
};
return Menu(entries, selected, option);
@@ -245,10 +252,12 @@ Component HMenu5(std::vector<std::string>* entries, int* selected) {
animation::easing::ElasticOut);
option.entries_option.transform = [](EntryState state) {
Element e = text(state.label) | hcenter | flex;
if (state.active && state.focused)
if (state.active && state.focused) {
e = e | bold;
if (!state.focused && !state.active)
}
if (!state.focused && !state.active) {
e = e | dim;
}
return e;
};
option.underline.color_inactive = Color::Default;

View File

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

View File

@@ -17,8 +17,9 @@ int main() {
std::vector<std::string> entries;
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));
}
auto radiobox = Radiobox(&entries, &selected);
auto renderer = Renderer(radiobox, [&] {
return radiobox->Render() | vscroll_indicator | frame |

View File

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

View File

@@ -32,10 +32,12 @@ int main() {
// Plot a function:
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));
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);
}
auto document = canvas(&c) | border;

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ if ("serviceWorker" in navigator && !window.crossOriginIsolated) {
const url_sw = new URL("./sw.js", location.href);
const registration = await navigator.serviceWorker.register(url_sw);
window.location.reload(); // Reload to ensure the COOP/COEP headers are set.
}
}
const example_list = "@EXAMPLES@".split(";");
const url_search_params = new URLSearchParams(window.location.search);
@@ -55,7 +55,7 @@ const stdout = code => {
const stderr = code => {
if (code == 0 || code == 10) {
console.error(String.fromCodePoint(...stderr_buffer));
stderr_buffer = [];
stderr_buffer.length = 0;
} else {
stderr_buffer.push(code)
}
@@ -89,9 +89,6 @@ window.Module = {
const resize_observer = new ResizeObserver(resize_handler);
resize_observer.observe(term_element);
resize_handler();
// Disable scrollbar
//term.write('\x1b[?47h')
},
};

View File

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

View File

@@ -9,8 +9,9 @@
#include <ftxui/dom/direction.hpp> // for Direction, Direction::Left, Direction::Right, Direction::Down
#include <ftxui/dom/elements.hpp> // for Element, separator
#include <ftxui/util/ref.hpp> // for Ref, ConstRef, StringRef
#include <functional> // for function
#include <string> // for string
#include <ftxui/util/warn_windows_macro.hpp>
#include <functional> // for function
#include <string> // for string
#include "ftxui/component/component_base.hpp" // for Component
#include "ftxui/screen/color.hpp" // for Color, Color::GrayDark, Color::White

View File

@@ -4,6 +4,7 @@
#ifndef FTXUI_COMPONENT_RECEIVER_HPP_
#define FTXUI_COMPONENT_RECEIVER_HPP_
#include <ftxui/util/warn_windows_macro.h>
#include <algorithm> // for copy, max
#include <atomic> // for atomic, __atomic_base
#include <condition_variable> // for condition_variable
@@ -14,6 +15,8 @@
namespace ftxui {
// Deprecated
//
// Usage:
//
// Initialization:
@@ -39,17 +42,24 @@ namespace ftxui {
// Receiver::Receive() returns true when there are no more senders.
// clang-format off
// Deprecated:
template<class T> class SenderImpl;
// Deprecated:
template<class T> class ReceiverImpl;
// Deprecated:
// Deprecated:
template<class T> using Sender = std::unique_ptr<SenderImpl<T>>;
// Deprecated:
template<class T> using Receiver = std::unique_ptr<ReceiverImpl<T>>;
// Deprecated:
template<class T> Receiver<T> MakeReceiver();
// clang-format on
// ---- Implementation part ----
template <class T>
// Deprecated:
class SenderImpl {
public:
SenderImpl(const SenderImpl&) = delete;

View File

@@ -4,12 +4,10 @@
#ifndef FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#include <atomic> // for atomic
#include <ftxui/component/receiver.hpp> // for Receiver, Sender
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#include <thread> // for thread
#include <atomic> // for atomic
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -26,6 +24,10 @@ struct Event;
using Component = std::shared_ptr<ComponentBase>;
class ScreenInteractivePrivate;
namespace task {
class TaskRunner;
}
/// @brief ScreenInteractive is a `Screen` that can handle events, run a main
/// loop, and manage components.
///
@@ -40,8 +42,12 @@ class ScreenInteractive : public Screen {
static ScreenInteractive FitComponent();
static ScreenInteractive TerminalOutput();
// Destructor.
~ScreenInteractive() override;
// Options. Must be called before Loop().
void TrackMouse(bool enable = true);
void HandlePipedInput(bool enable = true);
// Return the currently active screen, nullptr if none.
static ScreenInteractive* Active();
@@ -95,8 +101,14 @@ class ScreenInteractive : public Screen {
void Draw(Component component);
void ResetCursorPosition();
void InstallPipedInputHandling();
void Signal(int signal);
void FetchTerminalEvents();
void PostAnimationTask();
ScreenInteractive* suspended_screen_ = nullptr;
enum class Dimension {
FitComponent,
@@ -108,20 +120,16 @@ class ScreenInteractive : public Screen {
int dimx,
int dimy,
bool use_alternative_screen);
const Dimension dimension_;
const bool use_alternative_screen_;
bool track_mouse_ = true;
Sender<Task> task_sender_;
Receiver<Task> task_receiver_;
std::string set_cursor_position;
std::string reset_cursor_position;
std::atomic<bool> quit_{false};
std::thread event_listener_;
std::thread animation_listener_;
bool animation_requested_ = false;
animation::TimePoint previous_animation_time_;
@@ -137,6 +145,9 @@ class ScreenInteractive : public Screen {
bool force_handle_ctrl_c_ = 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.
int cursor_reset_shape_ = 1;
@@ -156,8 +167,14 @@ class ScreenInteractive : public Screen {
std::unique_ptr<Selection> selection_;
std::function<void()> selection_on_change_;
// PIMPL private implementation idiom (Pimpl).
struct Internal;
std::unique_ptr<Internal> internal_;
friend class Loop;
Component component_;
public:
class Private {
public:

View File

@@ -30,7 +30,7 @@ namespace ftxui {
/// - 2x4 braille characters (1x1 pixel)
/// - 2x2 block characters (2x2 pixels)
/// - 2x4 normal characters (2x4 pixels)
///
///
/// You need to multiply the x coordinate by 2 and the y coordinate by 4 to
/// get the correct position in the terminal.
///

View File

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

View File

@@ -11,7 +11,7 @@ namespace ftxui {
/// It is defined by its minimum and maximum coordinates along the x and y axes.
/// Note that the coordinates are inclusive, meaning that the box includes both
/// the minimum and maximum values.
///
///
/// @ingroup screen
struct Box {
int x_min = 0;

View File

@@ -20,6 +20,9 @@ class Image {
Image() = delete;
Image(int dimx, int dimy);
// Destructor:
virtual ~Image() = default;
// Access a character in the grid at a given position.
std::string& at(int x, int y);
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/terminal.hpp" // for Dimensions
#include "ftxui/util/autoreset.hpp" // for AutoReset
namespace ftxui {
@@ -31,6 +30,9 @@ class Screen : public Image {
static Screen Create(Dimensions dimension);
static Screen Create(Dimensions width, Dimensions height);
// Destructor:
~Screen() override = default;
std::string ToString() const;
// 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:
bool mouse_hover_ = false;
Box box_;
ButtonOption option_;
float animation_background_ = 0;
float animation_foreground_ = 0;
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
// the LICENSE file.
#include <cassert>
#include <ftxui/component/event.hpp>
#include <vector>
#include "ftxui/component/component.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) {
int index = 0;
while (index < size && data[index])
while (index < size && data[index]) {
++index;
}
auto out = std::string(data, 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) {
if (size == 0)
if (size == 0) {
return 0;
}
auto out = int(data[0]);
data++;
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) {
depth--;
int value = GeneratorInt(data, size);
if (depth <= 0)
if (depth <= 0) {
return Button(GeneratorString(data, size), [] {});
}
constexpr int value_max = 19;
value = (value % value_max + value_max) % value_max;
@@ -212,16 +216,17 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
auto screen =
Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height));
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (size_t i = 0; i < size; ++i)
parser.Add(data[i]);
// Generate some events.
std::vector<Event> events;
auto parser =
TerminalInputParser([&](const Event& event) { events.push_back(event); });
for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]);
}
Task event;
while (event_receiver->Receive(&event)) {
component->OnEvent(std::get<Event>(event));
for (const auto& event : events) {
component->OnEvent(event);
auto document = component->Render();
Render(screen, document);
}

View File

@@ -12,9 +12,15 @@ export module ftxui.component.receiver;
* @brief The FTXUI ftxui:: namespace
*/
export namespace ftxui {
// Deprecated:
using ftxui::SenderImpl;
// Deprecated:
using ftxui::ReceiverImpl;
// Deprecated:
using ftxui::Sender;
// Deprecated:
using ftxui::Receiver;
// Deprecated:
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

@@ -17,24 +17,22 @@
#include <memory>
#include <stack> // for stack
#include <string>
#include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore
#include <type_traits> // for decay_t
#include <utility> // for move, swap
#include <variant> // for visit, variant
#include <vector> // for vector
#include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore
#include <utility> // for move, swap
#include <variant> // for visit, variant
#include <vector> // for vector
#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event
#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/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/util.hpp" // for util::clamp
#include "ftxui/util/autoreset.hpp" // for AutoReset
#if defined(_WIN32)
#define DEFINE_CONSOLEV2_PROPERTIES
@@ -47,9 +45,11 @@
#error Must be compiled in UNICODE mode
#endif
#else
#include <fcntl.h>
#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 <unistd.h> // for STDIN_FILENO, read
#include <cerrno>
#endif
// Quick exit is missing in standard CLang headers
@@ -59,6 +59,20 @@
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 {
void RequestAnimationFrame() {
auto* screen = ScreenInteractive::Active();
@@ -82,73 +96,9 @@ constexpr int timeout_milliseconds = 20;
timeout_milliseconds * 1000;
#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__)
#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" {
EMSCRIPTEN_KEEPALIVE
void ftxui_on_resize(int columns, int rows) {
@@ -162,8 +112,8 @@ void ftxui_on_resize(int columns, int rows) {
#else // POSIX (Linux & Mac)
int CheckStdinReady(int usec_timeout) {
timeval tv = {0, usec_timeout}; // NOLINT
int CheckStdinReady() {
timeval tv = {0, 0}; // NOLINT
fd_set fds;
FD_ZERO(&fds); // NOLINT
FD_SET(STDIN_FILENO, &fds); // NOLINT
@@ -171,24 +121,6 @@ int CheckStdinReady(int usec_timeout) {
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
std::stack<Closure> on_exit_functions; // NOLINT
@@ -335,15 +267,6 @@ class CapturedMouseImpl : public CapturedMouseInterface {
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
ScreenInteractive::ScreenInteractive(Dimension dimension,
@@ -353,7 +276,8 @@ ScreenInteractive::ScreenInteractive(Dimension dimension,
: Screen(dimx, dimy),
dimension_(dimension),
use_alternative_screen_(use_alternative_screen) {
task_receiver_ = MakeReceiver<Task>();
internal_ = std::make_unique<Internal>(
[&](Event event) { PostEvent(std::move(event)); });
}
// static
@@ -381,10 +305,10 @@ ScreenInteractive ScreenInteractive::Fullscreen() {
ScreenInteractive ScreenInteractive::FullscreenPrimaryScreen() {
auto terminal = Terminal::Size();
return {
Dimension::Fullscreen,
terminal.dimx,
terminal.dimy,
/*use_alternative_screen=*/false,
Dimension::Fullscreen,
terminal.dimx,
terminal.dimy,
/*use_alternative_screen=*/false,
};
}
@@ -409,11 +333,13 @@ ScreenInteractive ScreenInteractive::TerminalOutput() {
return {
Dimension::TerminalOutput,
terminal.dimx,
terminal.dimy, // Best guess.
terminal.dimy, // Best guess.
/*use_alternative_screen=*/false,
};
}
ScreenInteractive::~ScreenInteractive() = default;
/// Create a ScreenInteractive whose width and height match the component being
/// drawn.
// static
@@ -421,8 +347,8 @@ ScreenInteractive ScreenInteractive::FitComponent() {
auto terminal = Terminal::Size();
return {
Dimension::FitComponent,
terminal.dimx, // Best guess.
terminal.dimy, // Best guess.
terminal.dimx, // Best guess.
terminal.dimy, // Best guess.
false,
};
}
@@ -446,16 +372,24 @@ void ScreenInteractive::TrackMouse(bool enable) {
track_mouse_ = enable;
}
/// @brief Enable or disable automatic piped input handling.
/// When enabled, FTXUI will detect piped input and redirect stdin 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.
/// It will be executed later, after every other scheduled tasks.
void ScreenInteractive::Post(Task task) {
// Task/Events sent toward inactive screen or screen waiting to become
// inactive are dropped.
if (!task_sender_) {
return;
}
task_sender_->Send(std::move(task));
internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
HandleTask(component_, task);
});
}
/// @brief Add an event to the main loop.
@@ -499,7 +433,7 @@ void ScreenInteractive::Loop(Component component) { // NOLINT
/// @brief Return whether the main loop has been quit.
bool ScreenInteractive::HasQuitted() {
return task_receiver_->HasQuitted();
return quit_;
}
// private
@@ -656,7 +590,15 @@ void ScreenInteractive::Install() {
SetConsoleMode(stdin_handle, in_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}) {
InstallSignalHandler(signal);
}
@@ -728,41 +670,102 @@ void ScreenInteractive::Install() {
// ensure it is fully applied:
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;
task_sender_ = task_receiver_->MakeSender();
event_listener_ =
std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
PostAnimationTask();
}
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
void ScreenInteractive::Uninstall() {
ExitNow();
event_listener_.join();
animation_listener_.join();
OnExit();
}
// private
// NOLINTNEXTLINE
void ScreenInteractive::RunOnceBlocking(Component component) {
ExecuteSignalHandlers();
Task task;
if (task_receiver_->Receive(&task)) {
HandleTask(component, task);
// Set FPS to 60 at most.
const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
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);
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);
}
}
RunOnce(component);
}
// private
void ScreenInteractive::RunOnce(Component component) {
Task task;
while (task_receiver_->ReceiveNonBlocking(&task)) {
HandleTask(component, task);
ExecuteSignalHandlers();
AutoReset set_component(&component_, component);
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_) {
selection_data_previous_ = selection_data_;
@@ -783,6 +786,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
// clang-format off
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
if (arg.is_cursor_position()) {
cursor_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y();
@@ -1034,7 +1038,6 @@ void ScreenInteractive::Exit() {
// private:
void ScreenInteractive::ExitNow() {
quit_ = true;
task_sender_.reset();
}
// private:
@@ -1067,6 +1070,118 @@ void ScreenInteractive::Signal(int signal) {
#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==(
const ScreenInteractive::SelectionData& other) const {
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

@@ -27,9 +27,10 @@ namespace {
// Capture the standard output (stdout) to a string.
class StdCapture {
public:
explicit StdCapture(std::string* captured)
: captured_(captured) {
if (pipe(pipefd_) != 0) return;
explicit StdCapture(std::string* captured) : captured_(captured) {
if (pipe(pipefd_) != 0) {
return;
}
old_stdout_ = dup(fileno(stdout));
fflush(stdout);
dup2(pipefd_[1], fileno(stdout));
@@ -188,9 +189,7 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
auto capture = StdCapture(&output);
auto screen = ScreenInteractive::FixedSize(2, 2);
auto component = Renderer([&] {
return text("AB");
});
auto component = Renderer([&] { return text("AB"); });
Loop loop(&screen, component);
loop.RunOnce();
@@ -241,7 +240,6 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
"\r\n"sv;
ASSERT_EQ(expected, output);
#endif
}
} // 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 <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 <memory> // for unique_ptr, allocator
#include <utility> // for move
@@ -90,7 +90,7 @@ const std::map<std::string, std::string> g_uniformize = {
{"\x1B[X", "\x1B[24~"}, // F12
};
TerminalInputParser::TerminalInputParser(Sender<Task> out)
TerminalInputParser::TerminalInputParser(std::function<void(Event)> out)
: out_(std::move(out)) {}
void TerminalInputParser::Timeout(int time) {
@@ -131,7 +131,7 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
return;
case CHARACTER:
out_->Send(Event::Character(std::move(pending_)));
out_(Event::Character(std::move(pending_)));
pending_.clear();
return;
@@ -140,25 +140,25 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
if (it != g_uniformize.end()) {
pending_ = it->second;
}
out_->Send(Event::Special(std::move(pending_)));
out_(Event::Special(std::move(pending_)));
pending_.clear();
}
return;
case MOUSE:
out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
pending_.clear();
return;
case CURSOR_POSITION:
out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
out_(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
pending_.clear();
return;
case CURSOR_SHAPE:
out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
out_(Event::CursorShape(std::move(pending_), output.cursor_shape));
pending_.clear();
return;
}

View File

@@ -4,12 +4,11 @@
#ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#include <functional>
#include <string> // for string
#include <vector> // for vector
#include "ftxui/component/mouse.hpp" // for Mouse
#include "ftxui/component/receiver.hpp" // for Sender
#include "ftxui/component/task.hpp" // for Task
#include "ftxui/component/mouse.hpp" // for Mouse
namespace ftxui {
struct Event;
@@ -17,7 +16,7 @@ struct Event;
// Parse a sequence of |char| accross |time|. Produces |Event|.
class TerminalInputParser {
public:
explicit TerminalInputParser(Sender<Task> out);
explicit TerminalInputParser(std::function<void(Event)> out);
void Timeout(int time);
void Add(char c);
@@ -62,7 +61,7 @@ class TerminalInputParser {
Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments);
Output ParseCursorPosition(std::vector<int> arguments);
Sender<Task> out_;
std::function<void(Event)> out_;
int position_ = -1;
int timeout_ = 0;
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
// the LICENSE file.
#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 <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/receiver.hpp" // for MakeReceiver, ReceiverImpl
#include "ftxui/component/terminal_input_parser.hpp"
#include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE
@@ -17,233 +17,205 @@ namespace ftxui {
// Test char |c| to are trivially converted into |Event::Character(c)|.
TEST(Event, Character) {
std::vector<char> basic_char;
for (char c = 'a'; c <= 'z'; ++c)
for (char c = 'a'; c <= 'z'; ++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);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (char c : basic_char)
parser.Add(c);
}
Task received;
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
for (char c : basic_char) {
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
EXPECT_EQ(c, std::get<Event>(received).character()[0]);
parser.Add(c);
}
EXPECT_FALSE(event_receiver->Receive(&received));
for (size_t i = 0; i < basic_char.size(); ++i) {
EXPECT_TRUE(received_events[i].is_character());
EXPECT_EQ(basic_char[i], received_events[i].character()[0]);
}
EXPECT_EQ(received_events.size(), basic_char.size());
}
TEST(Event, EscapeKeyWithoutWaiting) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_TRUE(received_events.empty());
}
TEST(Event, EscapeKeyNotEnoughWait) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Timeout(49);
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Timeout(49);
Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_TRUE(received_events.empty());
}
TEST(Event, EscapeKeyEnoughWait) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Timeout(50);
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Timeout(50);
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::Escape);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], Event::Escape);
}
TEST(Event, EscapeFast) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('a');
parser.Add('\x1B');
parser.Add('b');
parser.Timeout(49);
}
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::AltA);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::AltB);
EXPECT_FALSE(event_receiver->Receive(&received));
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('a');
parser.Add('');
parser.Add('b');
parser.Timeout(49);
EXPECT_EQ(2, received_events.size());
EXPECT_EQ(received_events[0], Event::AltA);
EXPECT_EQ(received_events[1], Event::AltB);
}
TEST(Event, MouseLeftClickPressed) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('0');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('0');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Pressed);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Pressed);
}
TEST(Event, MouseLeftMoved) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('3');
parser.Add('2');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('3');
parser.Add('2');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Moved);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Moved);
}
TEST(Event, MouseLeftClickReleased) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('0');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('m');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('0');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('m');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Released);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button);
EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(42, received_events[0].mouse().y);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Released);
}
TEST(Event, MouseReporting) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('R');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('R');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_cursor_position());
EXPECT_EQ(42, std::get<Event>(received).cursor_x());
EXPECT_EQ(12, std::get<Event>(received).cursor_y());
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_cursor_position());
EXPECT_EQ(42, received_events[0].cursor_x());
EXPECT_EQ(12, received_events[0].cursor_y());
}
TEST(Event, MouseMiddleClick) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('3');
parser.Add('3');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('3');
parser.Add('3');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Middle, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_EQ(Mouse::Middle, received_events[0].mouse().button);
EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(42, received_events[0].mouse().y);
}
TEST(Event, MouseRightClick) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('3');
parser.Add('4');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Add('[');
parser.Add('3');
parser.Add('4');
parser.Add(';');
parser.Add('1');
parser.Add('2');
parser.Add(';');
parser.Add('4');
parser.Add('2');
parser.Add('M');
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Right, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_mouse());
EXPECT_EQ(Mouse::Right, received_events[0].mouse().button);
EXPECT_EQ(12, received_events[0].mouse().x);
EXPECT_EQ(42, received_events[0].mouse().y);
}
TEST(Event, UTF8) {
@@ -313,31 +285,30 @@ TEST(Event, UTF8) {
};
for (auto test : kTestCase) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input)
parser.Add(input);
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
for (auto input : test.input) {
parser.Add(input);
}
Task received;
if (test.valid) {
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_character());
} else {
EXPECT_TRUE(received_events.empty());
}
EXPECT_FALSE(event_receiver->Receive(&received));
}
}
TEST(Event, NewLine) {
for (char newline : {'\r', '\n'}) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(newline);
}
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received) == Event::Return);
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(newline);
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0] == Event::Return);
}
}
@@ -348,8 +319,9 @@ TEST(Event, Control) {
};
std::vector<TestCase> cases;
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;
}
cases.push_back({char(i), false});
}
cases.push_back({char(24), false});
@@ -357,17 +329,16 @@ TEST(Event, Control) {
cases.push_back({char(127), false});
for (auto test : cases) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(test.input);
}
Task received;
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(test.input);
if (test.cancel) {
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_TRUE(received_events.empty());
} else {
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::Special({test.input}));
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], Event::Special({test.input}));
}
}
}
@@ -375,8 +346,9 @@ TEST(Event, Control) {
TEST(Event, Special) {
auto str = [](std::string input) {
std::vector<unsigned char> output;
for (auto it : input)
for (auto it : input) {
output.push_back(it);
}
return output;
};
@@ -385,10 +357,12 @@ TEST(Event, Special) {
Event expected;
} kTestCase[] = {
// Arrow (default cursor mode)
{str("\x1B[A"), Event::ArrowUp}, {str("\x1B[B"), Event::ArrowDown},
{str("\x1B[C"), Event::ArrowRight}, {str("\x1B[D"), Event::ArrowLeft},
{str("\x1B[H"), Event::Home}, {str("\x1B[F"), Event::End},
/*
{str(""), Event::ArrowUp},
{str(""), Event::ArrowDown},
{str(""), Event::ArrowRight},
{str(""), Event::ArrowLeft},
{str(""), Event::Home},
{str(""), Event::End},
// Arrow (application cursor mode)
{str("\x1BOA"), Event::ArrowUp},
@@ -469,45 +443,38 @@ TEST(Event, Special) {
// Custom:
{{0}, Event::Custom},
*/
};
for (auto test : kTestCase) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input) {
parser.Add(input);
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
for (auto input : test.input) {
parser.Add(input);
}
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), test.expected);
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], test.expected);
}
}
TEST(Event, DeviceControlString) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(27); // ESC
parser.Add(80); // P
parser.Add(49); // 1
parser.Add(36); // $
parser.Add(114); // r
parser.Add(49); // 1
parser.Add(32); // SP
parser.Add(113); // q
parser.Add(27); // ESC
parser.Add(92); // (backslash)
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(27); // ESC
parser.Add(80); // P
parser.Add(49); // 1
parser.Add(36); // $
parser.Add(114); // r
parser.Add(49); // 1
parser.Add(32); // SP
parser.Add(113); // q
parser.Add(27); // ESC
parser.Add(92); // (backslash)
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_cursor_shape());
EXPECT_EQ(1, std::get<Event>(received).cursor_shape());
EXPECT_FALSE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_cursor_shape());
EXPECT_EQ(1, received_events[0].cursor_shape());
}
} // namespace ftxui

View File

@@ -1,21 +1,16 @@
// Copyright 2021 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 <cstddef>
#include <ftxui/component/event.hpp>
#include "ftxui/component/terminal_input_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
using namespace ftxui;
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]);
}
auto parser = TerminalInputParser([&](Event) {});
for (size_t i = 0; i < size; ++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.
}

View File

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

View File

@@ -1284,8 +1284,9 @@ bool IsCombining(uint32_t ucs) {
}
bool IsFullWidth(uint32_t ucs) {
if (ucs < 0x0300) // Quick path: // NOLINT
if (ucs < 0x0300) { // Quick path: // NOLINT
return false;
}
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
// the LICENSE file.