10 Commits

Author SHA1 Message Date
PiGames
34ed175ed8 Merge 26083d492c into 68fc9b1212 2025-06-20 17:37:17 +01:00
PiGames
26083d492c Update install 2025-06-10 12:13:34 +03:00
PiGames
608972ea17 fixed g++ftxui 2025-06-10 12:02:25 +03:00
PiGames
cb199eeb41 fixed g++ftxui 2025-06-10 12:01:51 +03:00
PiGames
5b96a96ea1 Update README.md
install script explained
2025-06-10 11:52:28 +03:00
PiGames
a6165b9d55 install script added 2025-06-10 11:48:06 +03:00
PiGames
964b29cdfa Update README.md
g++ftxui bash script description added
2025-06-10 11:46:58 +03:00
PiGames
75b9387e95 bash script added for easy g++ usage 2025-06-10 11:43:23 +03:00
PiGames
0c50e80f1f LICENSE 2025-06-10 11:36:38 +03:00
PiGames
9cf19f13e2 LICENSE-old 2025-06-10 11:33:59 +03:00
62 changed files with 656 additions and 1430 deletions

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@@ -13,6 +13,7 @@ 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
@@ -158,18 +159,11 @@ 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",
@@ -190,6 +184,7 @@ ftxui_cc_library(
"include/ftxui/component/screen_interactive.hpp",
"include/ftxui/component/task.hpp",
],
linkopts = pthread_linkopts(),
deps = [
":dom",
":screen",
@@ -212,6 +207,7 @@ 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,7 +24,6 @@ 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,20 +144,26 @@ 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)
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()
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/">Examples</a> .
<a href="https://arthursonzogni.github.io/FTXUI/examples.html">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,8 +378,6 @@ 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.
@@ -437,9 +435,14 @@ If you don't, FTXUI may be used from the following packages:
[![Packaging status](https://repology.org/badge/vertical-allrepos/libftxui.svg)](https://repology.org/project/libftxui/versions)
If you choose to build and link FTXUI yourself, `ftxui-component` must be first in the linking order relative to the other FTXUI libraries, i.e.
If you choose to build with g++, you can use bash script:
1. Run the installer:
```bash
g++ . . . -lftxui-component -lftxui-dom -lftxui-screen . . .
chmod +x install && ./install
```
2. Compile your codes:
```bash
g++ftxui -o out source_code.cpp
```
To build FTXUI with modules, check [documentation](https://arthursonzogni.github.io/FTXUI/cpp20-modules.html)

View File

@@ -43,6 +43,16 @@ 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,7 +6,6 @@ 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,5 +101,6 @@ 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,10 +19,11 @@ 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
@@ -50,7 +51,6 @@ 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,24 +12,8 @@ FTXUI experimentally supports
compilation times and improve code organization. Each header has a
corresponding module.
Use the FTXUI_BUILD_MODULES option to build the FTXUI project itself to provide C++ 20 modules,
for example with CMake and Ninja:
**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;
@@ -42,25 +26,18 @@ int main() {
}
```
Note, the `ftxui` convenience module which simply pulls together all the modules:
```sh
cmake \
-DCMAKE_GENERATOR=Ninja \
-DFTXUI_BUILD_MODULES=ON \
..
```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
)
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**.
### Module list

View File

@@ -1,58 +0,0 @@
# 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,11 +15,15 @@ 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,64 +1,9 @@
// 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
#include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp"
#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;
int main(){
auto screen = ftxui::ScreenInteractive::Fullscreen();
auto testComponent = ftxui::Renderer([](){return ftxui::text("test Component");});
screen.Loop(testComponent);
return 0;
}

View File

@@ -133,9 +133,8 @@ 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,12 +82,10 @@ 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;
};
@@ -121,12 +119,10 @@ 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,12 +1,11 @@
// 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 <ftxui/component/loop.hpp>
#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 <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
@@ -270,7 +269,7 @@ int main() {
auto spinner_tab_renderer = Renderer([&] {
Elements entries;
for (int i = 0; i < 22; ++i) {
entries.push_back(spinner(i, shift / 5) | bold |
entries.push_back(spinner(i, shift / 2) | bold |
size(WIDTH, GREATER_THAN, 2) | border);
}
return hflow(std::move(entries));
@@ -513,20 +512,24 @@ int main() {
});
});
Loop loop(&screen, main_renderer);
while (!loop.HasQuitted()) {
// Update the state of the application.
shift++;
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);
}
});
// 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));
}
screen.Loop(main_renderer);
refresh_ui_continue = false;
refresh_ui.join();
return 0;
}

View File

@@ -22,12 +22,10 @@ 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,9 +17,8 @@ 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,9 +17,8 @@ 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,12 +116,10 @@ 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);
@@ -132,12 +130,10 @@ 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);
@@ -148,16 +144,13 @@ 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);
@@ -252,12 +245,10 @@ 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,9 +20,8 @@ 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,9 +17,8 @@ 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,11 +19,10 @@ 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.
@@ -34,11 +33,10 @@ 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,12 +32,10 @@ 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,9 +86,8 @@ 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.
@@ -139,9 +138,8 @@ 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,9 +21,8 @@ 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

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

8
g++ftxui Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
/usr/bin/g++ \
-I~/FTXUI/include \
-L~/FTXUI/build \
"$@" \
-lftxui-component -lftxui-dom -lftxui-screen

View File

@@ -8,7 +8,6 @@
#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,9 +9,8 @@
#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 <ftxui/util/warn_windows_macro.hpp>
#include <functional> // for function
#include <string> // for string
#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,7 +4,6 @@
#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
@@ -15,8 +14,6 @@
namespace ftxui {
// Deprecated
//
// Usage:
//
// Initialization:
@@ -42,24 +39,17 @@ 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,10 +4,12 @@
#ifndef FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP
#include <atomic> // for atomic
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#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 "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -24,10 +26,6 @@ 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.
///
@@ -42,12 +40,8 @@ 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();
@@ -101,14 +95,8 @@ 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,
@@ -120,16 +108,20 @@ 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_;
@@ -145,9 +137,6 @@ 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;
@@ -167,14 +156,8 @@ 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

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

View File

@@ -20,9 +20,6 @@ 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,6 +11,7 @@
#include "ftxui/screen/image.hpp" // for Pixel, Image
#include "ftxui/screen/terminal.hpp" // for Dimensions
#include "ftxui/util/autoreset.hpp" // for AutoReset
namespace ftxui {
@@ -30,9 +31,6 @@ 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

@@ -1,18 +0,0 @@
// 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_

3
install Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
sudo cp g++ftxui /usr/local/bin/ && sudo chmod +x /usr/local/bin/g++ftxui

View File

@@ -139,6 +139,7 @@ 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,7 +2,6 @@
// 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"
@@ -23,9 +22,8 @@ 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;
@@ -41,9 +39,8 @@ 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--;
@@ -115,9 +112,8 @@ 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;
@@ -216,17 +212,16 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
auto screen =
Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height));
// 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]);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (size_t i = 0; i < size; ++i)
parser.Add(data[i]);
}
for (const auto& event : events) {
component->OnEvent(event);
Task event;
while (event_receiver->Receive(&event)) {
component->OnEvent(std::get<Event>(event));
auto document = component->Render();
Render(screen, document);
}

View File

@@ -12,15 +12,9 @@ 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

@@ -0,0 +1,81 @@
// 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,22 +17,24 @@
#include <memory>
#include <stack> // for stack
#include <string>
#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 <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 "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/task_runner.hpp"
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
#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
@@ -45,11 +47,9 @@
#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,20 +59,6 @@
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();
@@ -96,9 +82,73 @@ 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) {
@@ -112,8 +162,8 @@ void ftxui_on_resize(int columns, int rows) {
#else // POSIX (Linux & Mac)
int CheckStdinReady() {
timeval tv = {0, 0}; // NOLINT
int CheckStdinReady(int usec_timeout) {
timeval tv = {0, usec_timeout}; // NOLINT
fd_set fds;
FD_ZERO(&fds); // NOLINT
FD_SET(STDIN_FILENO, &fds); // NOLINT
@@ -121,6 +171,24 @@ int CheckStdinReady() {
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
@@ -267,6 +335,15 @@ 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,
@@ -276,8 +353,7 @@ ScreenInteractive::ScreenInteractive(Dimension dimension,
: Screen(dimx, dimy),
dimension_(dimension),
use_alternative_screen_(use_alternative_screen) {
internal_ = std::make_unique<Internal>(
[&](Event event) { PostEvent(std::move(event)); });
task_receiver_ = MakeReceiver<Task>();
}
// static
@@ -305,10 +381,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,
};
}
@@ -333,13 +409,11 @@ 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
@@ -347,8 +421,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,
};
}
@@ -372,24 +446,16 @@ 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) {
internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
HandleTask(component_, 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));
}
/// @brief Add an event to the main loop.
@@ -433,7 +499,7 @@ void ScreenInteractive::Loop(Component component) { // NOLINT
/// @brief Return whether the main loop has been quit.
bool ScreenInteractive::HasQuitted() {
return quit_;
return task_receiver_->HasQuitted();
}
// private
@@ -590,15 +656,7 @@ void ScreenInteractive::Install() {
SetConsoleMode(stdin_handle, in_mode);
SetConsoleMode(stdout_handle, out_mode);
#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
#else
for (const int signal : {SIGWINCH, SIGTSTP}) {
InstallSignalHandler(signal);
}
@@ -670,102 +728,41 @@ 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;
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
task_sender_ = task_receiver_->MakeSender();
event_listener_ =
std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
}
// private
void ScreenInteractive::Uninstall() {
ExitNow();
event_listener_.join();
animation_listener_.join();
OnExit();
}
// private
// NOLINTNEXTLINE
void ScreenInteractive::RunOnceBlocking(Component component) {
// 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);
}
ExecuteSignalHandlers();
Task task;
if (task_receiver_->Receive(&task)) {
HandleTask(component, task);
}
RunOnce(component);
}
// private
void ScreenInteractive::RunOnce(Component component) {
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;
Task task;
while (task_receiver_->ReceiveNonBlocking(&task)) {
HandleTask(component, task);
ExecuteSignalHandlers();
}
ExecuteSignalHandlers();
Draw(component);
Draw(std::move(component));
if (selection_data_previous_ != selection_data_) {
selection_data_previous_ = selection_data_;
@@ -786,7 +783,6 @@ 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();
@@ -1038,6 +1034,7 @@ void ScreenInteractive::Exit() {
// private:
void ScreenInteractive::ExitNow() {
quit_ = true;
task_sender_.reset();
}
// private:
@@ -1070,118 +1067,6 @@ 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

@@ -1,220 +0,0 @@
// 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,10 +27,9 @@ 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));
@@ -189,7 +188,9 @@ 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();
@@ -240,6 +241,7 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
"\r\n"sv;
ASSERT_EQ(expected, output);
#endif
}
} // namespace ftxui

View File

@@ -1,19 +0,0 @@
// 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

@@ -1,40 +0,0 @@
// 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

@@ -1,53 +0,0 @@
// 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

@@ -1,37 +0,0 @@
// 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

@@ -1,75 +0,0 @@
// 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

@@ -1,46 +0,0 @@
// 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

@@ -1,94 +0,0 @@
// 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 <functional> // for std::function
#include <ftxui/component/receiver.hpp> // for SenderImpl, Sender
#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(std::function<void(Event)> out)
TerminalInputParser::TerminalInputParser(Sender<Task> out)
: out_(std::move(out)) {}
void TerminalInputParser::Timeout(int time) {
@@ -131,7 +131,7 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
return;
case CHARACTER:
out_(Event::Character(std::move(pending_)));
out_->Send(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_(Event::Special(std::move(pending_)));
out_->Send(Event::Special(std::move(pending_)));
pending_.clear();
}
return;
case MOUSE:
out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
pending_.clear();
return;
case CURSOR_POSITION:
out_(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
pending_.clear();
return;
case CURSOR_SHAPE:
out_(Event::CursorShape(std::move(pending_), output.cursor_shape));
out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
pending_.clear();
return;
}

View File

@@ -4,11 +4,12 @@
#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/mouse.hpp" // for Mouse
#include "ftxui/component/receiver.hpp" // for Sender
#include "ftxui/component/task.hpp" // for Task
namespace ftxui {
struct Event;
@@ -16,7 +17,7 @@ struct Event;
// Parse a sequence of |char| accross |time|. Produces |Event|.
class TerminalInputParser {
public:
explicit TerminalInputParser(std::function<void(Event)> out);
explicit TerminalInputParser(Sender<Task> out);
void Timeout(int time);
void Add(char c);
@@ -61,7 +62,7 @@ class TerminalInputParser {
Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments);
Output ParseCursorPosition(std::vector<int> arguments);
std::function<void(Event)> out_;
Sender<Task> 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 <functional> // for function
#include <ftxui/component/task.hpp> // for Task
#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,205 +17,233 @@ 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);
}
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
Task received;
for (char c : basic_char) {
parser.Add(c);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
EXPECT_EQ(c, std::get<Event>(received).character()[0]);
}
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());
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, EscapeKeyWithoutWaiting) {
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
}
EXPECT_TRUE(received_events.empty());
Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, EscapeKeyNotEnoughWait) {
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Timeout(49);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Timeout(49);
}
EXPECT_TRUE(received_events.empty());
Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, EscapeKeyEnoughWait) {
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add('');
parser.Timeout(50);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Timeout(50);
}
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], Event::Escape);
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::Escape);
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, EscapeFast) {
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);
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));
}
TEST(Event, MouseLeftClickPressed) {
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');
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');
}
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);
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));
}
TEST(Event, MouseLeftMoved) {
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');
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');
}
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);
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));
}
TEST(Event, MouseLeftClickReleased) {
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');
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');
}
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);
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));
}
TEST(Event, MouseReporting) {
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');
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');
}
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());
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));
}
TEST(Event, MouseMiddleClick) {
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');
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');
}
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);
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));
}
TEST(Event, MouseRightClick) {
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');
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');
}
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);
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));
}
TEST(Event, UTF8) {
@@ -285,30 +313,31 @@ TEST(Event, UTF8) {
};
for (auto test : kTestCase) {
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);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input)
parser.Add(input);
}
Task received;
if (test.valid) {
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_character());
} else {
EXPECT_TRUE(received_events.empty());
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
}
EXPECT_FALSE(event_receiver->Receive(&received));
}
}
TEST(Event, NewLine) {
for (char newline : {'\r', '\n'}) {
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);
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);
}
}
@@ -319,9 +348,8 @@ 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});
@@ -329,16 +357,17 @@ TEST(Event, Control) {
cases.push_back({char(127), false});
for (auto test : cases) {
std::vector<Event> received_events;
auto parser = TerminalInputParser(
[&](Event event) { received_events.push_back(std::move(event)); });
parser.Add(test.input);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(test.input);
}
Task received;
if (test.cancel) {
EXPECT_TRUE(received_events.empty());
EXPECT_FALSE(event_receiver->Receive(&received));
} else {
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], Event::Special({test.input}));
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::Special({test.input}));
}
}
}
@@ -346,9 +375,8 @@ 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;
};
@@ -357,12 +385,10 @@ TEST(Event, Special) {
Event expected;
} kTestCase[] = {
// Arrow (default cursor mode)
{str(""), Event::ArrowUp},
{str(""), Event::ArrowDown},
{str(""), Event::ArrowRight},
{str(""), Event::ArrowLeft},
{str(""), Event::Home},
{str(""), Event::End},
{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},
/*
// Arrow (application cursor mode)
{str("\x1BOA"), Event::ArrowUp},
@@ -443,38 +469,45 @@ TEST(Event, Special) {
// Custom:
{{0}, Event::Custom},
*/
};
for (auto test : kTestCase) {
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);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input) {
parser.Add(input);
}
}
EXPECT_EQ(1, received_events.size());
EXPECT_EQ(received_events[0], test.expected);
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), test.expected);
EXPECT_FALSE(event_receiver->Receive(&received));
}
}
TEST(Event, DeviceControlString) {
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)
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)
}
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_cursor_shape());
EXPECT_EQ(1, received_events[0].cursor_shape());
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));
}
} // namespace ftxui

View File

@@ -1,16 +1,21 @@
// 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 parser = TerminalInputParser([&](Event) {});
for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]);
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
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,9 +47,8 @@ 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,9 +1284,8 @@ 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 2025 Arthur Sonzogni. All rights reserved.
// 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.