FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
screen_interactive.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// このソースコードの使用は、LICENSE ファイルにあるMITライセンスによって管理されています。
4#include <algorithm> // for copy, max, min
5#include <array> // for array
6#include <atomic>
7#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
8#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
9#include <cstdint>
10#include <cstdio> // for fileno, stdin
11#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
12#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
13#include <functional> // for function
14#include <initializer_list> // for initializer_list
15#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
16#include <memory>
17#include <stack> // for stack
18#include <string>
19#include <thread> // for thread, sleep_for
20#include <tuple> // for _Swallow_assign, ignore
21#include <utility> // for move, swap
22#include <variant> // for visit, variant
23#include <vector> // for vector
24#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
25#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
26#include "ftxui/component/component_base.hpp" // for ComponentBase
27#include "ftxui/component/event.hpp" // for Event
28#include "ftxui/component/loop.hpp" // for Loop
30#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
31#include "ftxui/dom/node.hpp" // for Node, Render
32#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
33#include "ftxui/screen/util.hpp" // for util::clamp
34#include "ftxui/util/autoreset.hpp" // for AutoReset
35
36#if defined(_WIN32)
37#define DEFINE_CONSOLEV2_PROPERTIES
38#define WIN32_LEAN_AND_MEAN
39#ifndef NOMINMAX
40#define NOMINMAX
41#endif
42#include <windows.h>
43#ifndef UNICODE
44#error Must be compiled in UNICODE mode
45#endif
46#else
47#include <fcntl.h>
48#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
49#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
50#include <unistd.h> // for STDIN_FILENO, read
51#include <cerrno>
52#endif
53
54// Quick exit is missing in standard CLang headers
55#if defined(__clang__) && defined(__APPLE__)
56#define quick_exit(a) exit(a)
57#endif
58
59namespace ftxui {
60
61struct ScreenInteractive::Internal {
62 // 文字をイベントに変換します。
63 TerminalInputParser terminal_input_parser;
64
65 task::TaskRunner task_runner;
66
67 // 文字が最後に受信された時刻。
68 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
69 std::chrono::steady_clock::now();
70
71 explicit Internal(std::function<void(Event)> out)
72 : terminal_input_parser(std::move(out)) {}
73};
74
75namespace animation {
77 auto* screen = ScreenInteractive::Active();
78 if (screen) {
79 screen->RequestAnimationFrame();
80 }
81}
82} // namespace animation
83
84namespace {
85
86ScreenInteractive* g_active_screen = nullptr; // NOLINT
87
88void Flush() {
89 // Emscripten doesn't implement flush. We interpret zero as flush.
90 std::cout << '\0' << std::flush;
91}
92
93constexpr int timeout_milliseconds = 20;
94[[maybe_unused]] constexpr int timeout_microseconds =
95 timeout_milliseconds * 1000;
96#if defined(_WIN32)
97
98#elif defined(__EMSCRIPTEN__)
99#include <emscripten.h>
100
101extern "C" {
102EMSCRIPTEN_KEEPALIVE
103void ftxui_on_resize(int columns, int rows) {
105 columns,
106 rows,
107 });
108 std::raise(SIGWINCH);
109}
110}
111
112#else // POSIX (Linux & Mac)
113
114int CheckStdinReady(int fd) {
115 timeval tv = {0, 0}; // NOLINT
116 fd_set fds;
117 FD_ZERO(&fds); // NOLINT
118 FD_SET(fd, &fds); // NOLINT
119 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
120 return FD_ISSET(fd, &fds); // NOLINT
121}
122
123#endif
124
125std::stack<Closure> on_exit_functions; // NOLINT
126void OnExit() {
127 while (!on_exit_functions.empty()) {
128 on_exit_functions.top()();
129 on_exit_functions.pop();
130 }
131}
132
133std::atomic<int> g_signal_exit_count = 0; // NOLINT
134#if !defined(_WIN32)
135std::atomic<int> g_signal_stop_count = 0; // NOLINT
136std::atomic<int> g_signal_resize_count = 0; // NOLINT
137#endif
138
139// Async signal safe function
140void RecordSignal(int signal) {
141 switch (signal) {
142 case SIGABRT:
143 case SIGFPE:
144 case SIGILL:
145 case SIGINT:
146 case SIGSEGV:
147 case SIGTERM:
148 g_signal_exit_count++;
149 break;
150
151#if !defined(_WIN32)
152 case SIGTSTP: // NOLINT
153 g_signal_stop_count++;
154 break;
155
156 case SIGWINCH: // NOLINT
157 g_signal_resize_count++;
158 break;
159#endif
160
161 default:
162 break;
163 }
164}
165
166void ExecuteSignalHandlers() {
167 int signal_exit_count = g_signal_exit_count.exchange(0);
168 while (signal_exit_count--) {
169 ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
170 }
171
172#if !defined(_WIN32)
173 int signal_stop_count = g_signal_stop_count.exchange(0);
174 while (signal_stop_count--) {
175 ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
176 }
177
178 int signal_resize_count = g_signal_resize_count.exchange(0);
179 while (signal_resize_count--) {
180 ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
181 }
182#endif
183}
184
185void InstallSignalHandler(int sig) {
186 auto old_signal_handler = std::signal(sig, RecordSignal);
187 on_exit_functions.emplace(
188 [=] { std::ignore = std::signal(sig, old_signal_handler); });
189}
190
191// CSI: Control Sequence Introducer
192const std::string CSI = "\x1b["; // NOLINT
193 //
194// DCS: Device Control String
195const std::string DCS = "\x1bP"; // NOLINT
196// ST: String Terminator
197const std::string ST = "\x1b\\"; // NOLINT
198
199// DECRQSS: Request Status String
200// DECSCUSR: Set Cursor Style
201const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
202
203// DEC: Digital Equipment Corporation
204enum class DECMode : std::uint16_t {
205 kLineWrap = 7,
206 kCursor = 25,
207
208 kMouseX10 = 9,
209 kMouseVt200 = 1000,
210 kMouseVt200Highlight = 1001,
211
212 kMouseBtnEventMouse = 1002,
213 kMouseAnyEvent = 1003,
214
215 kMouseUtf8 = 1005,
216 kMouseSgrExtMode = 1006,
217 kMouseUrxvtMode = 1015,
218 kMouseSgrPixelsMode = 1016,
219 kAlternateScreen = 1049,
220};
221
222// Device Status Report (DSR) {
223enum class DSRMode : std::uint8_t {
224 kCursor = 6,
225};
226
227std::string Serialize(const std::vector<DECMode>& parameters) {
228 bool first = true;
229 std::string out;
230 for (const DECMode parameter : parameters) {
231 if (!first) {
232 out += ";";
233 }
234 out += std::to_string(int(parameter));
235 first = false;
236 }
237 return out;
238}
239
240// DEC Private Mode Set (DECSET)
241std::string Set(const std::vector<DECMode>& parameters) {
242 return CSI + "?" + Serialize(parameters) + "h";
243}
244
245// DEC Private Mode Reset (DECRST)
246std::string Reset(const std::vector<DECMode>& parameters) {
247 return CSI + "?" + Serialize(parameters) + "l";
248}
249
250// Device Status Report (DSR)
251std::string DeviceStatusReport(DSRMode ps) {
252 return CSI + std::to_string(int(ps)) + "n";
253}
254
255class CapturedMouseImpl : public CapturedMouseInterface {
256 public:
257 explicit CapturedMouseImpl(std::function<void(void)> callback)
258 : callback_(std::move(callback)) {}
259 ~CapturedMouseImpl() override { callback_(); }
260 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
261 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
262 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
263 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
264
265 private:
266 std::function<void(void)> callback_;
267};
268
269} // namespace
270
271ScreenInteractive::ScreenInteractive(Dimension dimension,
272 int dimx,
273 int dimy,
274 bool use_alternative_screen)
275 : Screen(dimx, dimy),
276 dimension_(dimension),
277 use_alternative_screen_(use_alternative_screen) {
278 internal_ = std::make_unique<Internal>(
279 [&](Event event) { PostEvent(std::move(event)); });
280}
281
282// static
284 return {
285 Dimension::Fixed,
286 dimx,
287 dimy,
288 /*use_alternative_screen=*/false,
289 };
290}
291
292/// ターミナルサイズの全体を使用するScreenInteractiveを作成します。これは、ターミナルコンテンツを乱すことを避けるために、代替スクリーンバッファを使用します。
293/// @note これは`ScreenInteractive::FullscreenAlternateScreen()`と同じです。
294// static
298
299/// ターミナルサイズの全体を使用するScreenInteractiveを作成します。プライマリスクリーンバッファが使用されます。これは、ターミナルがリサイズされた場合、以前のコンテンツがターミナルコンテンツを乱す可能性があることを意味します。
300// static
302 auto terminal = Terminal::Size();
303 return {
304 Dimension::Fullscreen,
305 terminal.dimx,
306 terminal.dimy,
307 /*use_alternative_screen=*/false,
308 };
309}
310
311/// ターミナルサイズの全体を使用するScreenInteractiveを作成します。これは、ターミナルコンテンツを乱すことを避けるために、代替スクリーンバッファを使用します。
312// static
314 auto terminal = Terminal::Size();
315 return {
316 Dimension::Fullscreen,
317 terminal.dimx,
318 terminal.dimy,
319 /*use_alternative_screen=*/true,
320 };
321}
322
323/// ターミナル出力の幅に一致し、描画されるコンポーネントの高さに一致するScreenInteractiveを作成します。
324// static
326 auto terminal = Terminal::Size();
327 return {
328 Dimension::TerminalOutput,
329 terminal.dimx,
330 terminal.dimy, // Best guess.
331 /*use_alternative_screen=*/false,
332 };
333}
334
336
337/// 描画されるコンポーネントの幅と高さに一致するScreenInteractiveを作成します。
338// static
340 auto terminal = Terminal::Size();
341 return {
342 Dimension::FitComponent,
343 terminal.dimx, // Best guess.
344 terminal.dimy, // Best guess.
345 false,
346 };
347}
348
349/// @brief マウスが追跡され、イベントが報告されるかどうかを設定します。
350/// メインループの外側で呼び出されます。例: `ScreenInteractive::Loop(...)`。
351/// @param enable マウスイベントの追跡を有効にするかどうか。
352/// @note これはメインループの外側で呼び出す必要があります。例: `ScreenInteractive::Loop`を呼び出す前。
353/// @note マウストラッキングはデフォルトで有効になっています。
354/// @note マウストラッキングは、それをサポートする端末でのみサポートされています。
355///
356/// ### 例
357///
358/// ```cpp
359/// auto screen = ScreenInteractive::TerminalOutput();
360/// screen.TrackMouse(false);
361/// screen.Loop(component);
362/// ```
364 track_mouse_ = enable;
365}
366
367/// @brief 自動パイプ入力処理を有効または無効にします。
368/// 有効にすると、FTXUIはパイプ入力を検出し、キーボード入力のためにstdinを`/dev/tty`からリダイレクトし、アプリケーションがパイプデータを読み取りながら、対話型キーボードイベントを引き続き受信できるようにします。
369/// @param enable パイプ入力処理を有効にするかどうか。デフォルトはtrueです。
370/// @note これは`Loop()`の前に呼び出す必要があります。
371/// @note この機能はデフォルトで有効になっています。
372/// @note この機能はPOSIXシステム(Linux/macOS)でのみ利用可能です。
374 handle_piped_input_ = enable;
375}
376
377/// @brief メインループにタスクを追加します。
378/// これは、他のすべてのスケジュールされたタスクの後に実行されます。
380 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
381 HandleTask(component_, task);
382 });
383}
384
385/// @brief メインループにイベントを追加します。
386/// これは、他のすべてのスケジュールされたイベントの後に実行されます。
388 Post(event);
389}
390
391/// @brief すべてのアニメーションが完了するまで、画面をもう一度描画するタスクを追加します。
393 if (animation_requested_) {
394 return;
395 }
396 animation_requested_ = true;
397 auto now = animation::Clock::now();
398 const auto time_histeresis = std::chrono::milliseconds(33);
399 if (now - previous_animation_time_ >= time_histeresis) {
400 previous_animation_time_ = now;
401 }
402}
403
404/// @brief マウスをキャプチャできることに関するユニークロックを取得しようとします。
405/// @return マウスがまだキャプチャされていない場合はユニークロック、それ以外の場合はヌル。
407 if (mouse_captured) {
408 return nullptr;
409 }
410 mouse_captured = true;
411 return std::make_unique<CapturedMouseImpl>(
412 [this] { mouse_captured = false; });
413}
414
415/// @brief メインループを実行します。
416/// @param component 描画するコンポーネント。
417void ScreenInteractive::Loop(Component component) { // NOLINT
418 class Loop loop(this, std::move(component));
419 loop.Run();
420}
421
422/// @brief メインループが終了したかどうかを返します。
423bool ScreenInteractive::HasQuitted() {
424 return quit_;
425}
426
427// private
428void ScreenInteractive::PreMain() {
429 // Suspend previously active screen:
430 if (g_active_screen) {
431 std::swap(suspended_screen_, g_active_screen);
432 // Reset cursor position to the top of the screen and clear the screen.
433 suspended_screen_->ResetCursorPosition();
434 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
435 suspended_screen_->dimx_ = 0;
436 suspended_screen_->dimy_ = 0;
437
438 // Reset dimensions to force drawing the screen again next time:
439 suspended_screen_->Uninstall();
440 }
441
442 // This screen is now active:
443 g_active_screen = this;
444 g_active_screen->Install();
445
446 previous_animation_time_ = animation::Clock::now();
447}
448
449// private
450void ScreenInteractive::PostMain() {
451 // Put cursor position at the end of the drawing.
452 ResetCursorPosition();
453
454 g_active_screen = nullptr;
455
456 // Restore suspended screen.
457 if (suspended_screen_) {
458 // Clear screen, and put the cursor at the beginning of the drawing.
459 std::cout << ResetPosition(/*clear=*/true);
460 dimx_ = 0;
461 dimy_ = 0;
462 Uninstall();
463 std::swap(g_active_screen, suspended_screen_);
464 g_active_screen->Install();
465 } else {
466 Uninstall();
467
468 std::cout << '\r';
469 // On final exit, keep the current drawing and reset cursor position one
470 // line after it.
471 if (!use_alternative_screen_) {
472 std::cout << '\n';
473 std::cout << std::flush;
474 }
475 }
476}
477
478/// @brief 関数を装飾します。それは同じように実行されますが、実行中に現在アクティブなスクリーンターミナルフックは一時的にアンインストールされます。
479/// @param fn 装飾する関数。
481 return [this, fn] {
482 Uninstall();
483 fn();
484 Install();
485 };
486}
487
488/// @brief コンポーネントが`Event::CtrlC`をキャッチした場合でも、FTXUIにCtrl-Cを処理させるか処理させないかを強制します。
490 force_handle_ctrl_c_ = force;
491}
492
493/// @brief コンポーネントが`Event::CtrlZ`をキャッチした場合でも、FTXUIにCtrl-Zを処理させるか処理させないかを強制します。
495 force_handle_ctrl_z_ = force;
496}
497
498/// @brief 現在の選択内容を返します
500 if (!selection_) {
501 return "";
502 }
503 return selection_->GetParts();
504}
505
506void ScreenInteractive::SelectionChange(std::function<void()> callback) {
507 selection_on_change_ = std::move(callback);
508}
509
510/// @brief 現在アクティブな画面を返します。アクティブな画面がない場合はヌルを返します。
511// static
513 return g_active_screen;
514}
515
516// private
517void ScreenInteractive::Install() {
518 frame_valid_ = false;
519
520 // ユーザーが以前にプリントした内容が、ターミナル設定を変更する前に完全に適用されるように、stdoutのバッファをフラッシュします。これは、以下のターミナルエミュレータと通信するために2つの異なるチャネル(stdoutとtermios/WinAPI)を使用しているため重要です。詳細はhttps://github.com/ArthurSonzogni/FTXUI/issues/846を参照してください。
521 Flush();
522
523 InstallPipedInputHandling();
524
525 // 新しい設定をアンインストールした後、それが完全に適用されるようにターミナルにフラッシュします。
526 on_exit_functions.emplace([] { Flush(); });
527
528 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
529
530 // ターミナルに現在のカーソル形状を報告するように要求します。終了時にそれを復元します。
531 std::cout << DECRQSS_DECSCUSR;
532 on_exit_functions.emplace([this] {
533 std::cout << "\033[?25h"; // Enable cursor.
534 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
535 });
536
537 // 終了時にターミナル状態を復元するためのシグナルハンドラをインストールします。デフォルトのシグナルハンドラは終了時に復元されます。
538 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
539 InstallSignalHandler(signal);
540 }
541
542// 古いターミナル設定を保存し、終了時に復元します。
543#if defined(_WIN32)
544 // stdout および stdin で VT 処理を有効にする
545 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
546 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
547
548 DWORD out_mode = 0;
549 DWORD in_mode = 0;
550 GetConsoleMode(stdout_handle, &out_mode);
551 GetConsoleMode(stdin_handle, &in_mode);
552 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
553 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
554
555 // https://docs.microsoft.com/ja-jp/windows/console/setconsolemode
556 const int enable_virtual_terminal_processing = 0x0004;
557 const int disable_newline_auto_return = 0x0008;
558 out_mode |= enable_virtual_terminal_processing;
559 out_mode |= disable_newline_auto_return;
560
561 // https://docs.microsoft.com/ja-jp/windows/console/setconsolemode
562 const int enable_line_input = 0x0002;
563 const int enable_echo_input = 0x0004;
564 const int enable_virtual_terminal_input = 0x0200;
565 const int enable_window_input = 0x0008;
566 in_mode &= ~enable_echo_input;
567 in_mode &= ~enable_line_input;
568 in_mode |= enable_virtual_terminal_input;
569 in_mode |= enable_window_input;
570
571 SetConsoleMode(stdin_handle, in_mode);
572 SetConsoleMode(stdout_handle, out_mode);
573#else // POSIX (Linux & Mac)
574 // #if defined(__EMSCRIPTEN__)
575 //// Reading stdin isn't blocking.
576 // int flags = fcntl(0, F_GETFL, 0);
577 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
578
579 //// Restore the terminal configuration on exit.
580 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
581 // #endif
582 for (const int signal : {SIGWINCH, SIGTSTP}) {
583 InstallSignalHandler(signal);
584 }
585
586 struct termios terminal; // NOLINT
587 tcgetattr(tty_fd_, &terminal);
588 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
589 tcsetattr(tty_fd_, TCSANOW, &terminal);
590 });
591
592 // rawターミナル入力モードを有効にする
593 terminal.c_iflag &= ~IGNBRK; // ブレーク条件の無視を無効にする
594 terminal.c_iflag &= ~BRKINT; // 入出力が
595 // フラッシュされる原因となるブレークを無効にする
596 terminal.c_iflag &= ~PARMRK; // パリティエラーのマーキングを無効にする。
597 terminal.c_iflag &= ~ISTRIP; // 文字から8ビット目をストライピングするのを無効にする。
598 terminal.c_iflag &= ~INLCR; // NLからCRへのマッピングを無効にする。
599 terminal.c_iflag &= ~IGNCR; // CRの無視を無効にする。
600 terminal.c_iflag &= ~ICRNL; // CRからNLへのマッピングを無効にする。
601 terminal.c_iflag &= ~IXON; // 出力でのXON/XOFFフロー制御を無効にする
602
603 terminal.c_lflag &= ~ECHO; // 入力文字のエコーを無効にする。
604 terminal.c_lflag &= ~ECHONL; // 改行文字のエコーを無効にする。
605 terminal.c_lflag &= ~ICANON; // Canonicalモードを無効にする。
606 terminal.c_lflag &= ~ISIG; // 以下のキーを押したときにシグナルを送信するのを無効にする:
607 // - => DSUSP
608 // - C-Z => SUSP
609 // - C-C => INTR
610 // - C-d => QUIT
611 terminal.c_lflag &= ~IEXTEN; // 拡張入力処理を無効にする
612 terminal.c_cflag |= CS8; // バイトあたり8ビット
613
614 terminal.c_cc[VMIN] = 0; // 非canonical
615 // 読み取りのための最小文字数。
616 terminal.c_cc[VTIME] = 0; // 非canonical読み取りのためのデシ秒単位のタイムアウト。
617
618 tcsetattr(tty_fd_, TCSANOW, &terminal);
619
620#endif
621
622 auto enable = [&](const std::vector<DECMode>& parameters) {
623 std::cout << Set(parameters);
624 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
625 };
626
627 auto disable = [&](const std::vector<DECMode>& parameters) {
628 std::cout << Reset(parameters);
629 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
630 };
631
632 if (use_alternative_screen_) {
633 enable({
634 DECMode::kAlternateScreen,
635 });
636 }
637
638 disable({
639 // DECMode::kCursor,
640 DECMode::kLineWrap,
641 });
642
643 if (track_mouse_) {
644 enable({DECMode::kMouseVt200});
645 enable({DECMode::kMouseAnyEvent});
646 enable({DECMode::kMouseUrxvtMode});
647 enable({DECMode::kMouseSgrExtMode});
648 }
649
650 // 新しい設定をインストールした後、それが完全に適用されるようにターミナルにフラッシュします。
651 Flush();
652
653 quit_ = false;
654
655 PostAnimationTask();
656}
657
658void ScreenInteractive::InstallPipedInputHandling() {
659#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
660 tty_fd_ = STDIN_FILENO;
661 // アプリケーションによって明示的に有効にされている場合、パイプ入力リダイレクトを処理します。
662 // これにより、アプリケーションはstdinからデータを読み取りながら、対話的に使用するためにターミナルからキーボード入力を引き続き受信できます。
663 if (!handle_piped_input_) {
664 return;
665 }
666
667 // stdinがターミナルの場合、`/dev/tty`を開く必要はありません。
668 if (isatty(STDIN_FILENO)) {
669 return;
670 }
671
672 // キーボード入力のために`/dev/tty`を開きます。
673 tty_fd_ = open("/dev/tty", O_RDONLY);
674 if (tty_fd_ < 0) {
675 // `/dev/tty`のオープンに失敗しました(コンテナ、ヘッドレスシステムなど)
676 // stdinにフォールバックします。 return;
677 }
678
679 // 終了時に`/dev/tty`ファイルディスクリプタを閉じます。
680 on_exit_functions.emplace([this] {
681 close(tty_fd_);
682 tty_fd_ = -1;
683 });
684#endif
685}
686
687// private
688void ScreenInteractive::Uninstall() {
689 ExitNow();
690 OnExit();
691}
692
693// private
694// NOLINTNEXTLINE
695void ScreenInteractive::RunOnceBlocking(Component component) {
696 // 最大で60FPSに設定します。
697 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
698
699 auto time = std::chrono::steady_clock::now();
700 size_t executed_task = internal_->task_runner.ExecutedTasks();
701
702 // 少なくとも1つのタスクが実行されるのを待ちます。
703 while (executed_task == internal_->task_runner.ExecutedTasks() &&
704 !HasQuitted()) {
705 RunOnce(component);
706
707 const auto now = std::chrono::steady_clock::now();
708 const auto delta = now - time;
709 time = now;
710
711 if (delta < time_per_frame) {
712 const auto sleep_duration = time_per_frame - delta;
713 std::this_thread::sleep_for(sleep_duration);
714 }
715 }
716}
717
718// private
719void ScreenInteractive::RunOnce(Component component) {
720 AutoReset set_component(&component_, component);
721 ExecuteSignalHandlers();
722 FetchTerminalEvents();
723
724 // キューから保留中のタスクを実行します。
725 const size_t executed_task = internal_->task_runner.ExecutedTasks();
726 internal_->task_runner.RunUntilIdle();
727 // 実行されたタスクがない場合、画面を再描画せずに早期にリターンできます。
728 if (executed_task == internal_->task_runner.ExecutedTasks()) {
729 return;
730 }
731
732 ExecuteSignalHandlers();
733 Draw(component);
734
735 if (selection_data_previous_ != selection_data_) {
736 selection_data_previous_ = selection_data_;
737 if (selection_on_change_) {
738 selection_on_change_();
740 }
741 }
742}
743
744// private
745// NOLINTNEXTLINE
746void ScreenInteractive::HandleTask(Component component, Task& task) {
747 std::visit(
748 [&](auto&& arg) {
749 using T = std::decay_t<decltype(arg)>;
750
751 // clang-format off
752 // イベントを処理します。
753 if constexpr (std::is_same_v<T, Event>) {
754
755 if (arg.is_cursor_position()) {
756 cursor_x_ = arg.cursor_x();
757 cursor_y_ = arg.cursor_y();
758 return;
759 }
760
761 if (arg.is_cursor_shape()) {
762 cursor_reset_shape_= arg.cursor_shape();
763 return;
764 }
765
766 if (arg.is_mouse()) {
767 arg.mouse().x -= cursor_x_;
768 arg.mouse().y -= cursor_y_;
769 }
770
771 arg.screen_ = this;
772
773 bool handled = component->OnEvent(arg);
774
775 handled = HandleSelection(handled, arg);
776
777 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
778 RecordSignal(SIGABRT);
779 }
780
781#if !defined(_WIN32)
782 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
783 RecordSignal(SIGTSTP);
784 }
785#endif
786
787 frame_valid_ = false;
788 return;
789 }
790
791 // コールバックを処理します
792 if constexpr (std::is_same_v<T, Closure>) {
793 arg();
794 return;
795 }
796
797 // アニメーションを処理します
798 if constexpr (std::is_same_v<T, AnimationTask>) {
799 if (!animation_requested_) {
800 return;
801 }
802
803 animation_requested_ = false;
804 const animation::TimePoint now = animation::Clock::now();
805 const animation::Duration delta = now - previous_animation_time_;
806 previous_animation_time_ = now;
807
808 animation::Params params(delta);
809 component->OnAnimation(params);
810 frame_valid_ = false;
811 return;
812 }
813 },
814 task);
815 // clang-format on
816}
817
818// private
819bool ScreenInteractive::HandleSelection(bool handled, Event event) {
820 if (handled) {
821 selection_pending_ = nullptr;
822 selection_data_.empty = true;
823 selection_ = nullptr;
824 return true;
825 }
826
827 if (!event.is_mouse()) {
828 return false;
829 }
830
831 auto& mouse = event.mouse();
832 if (mouse.button != Mouse::Left) {
833 return false;
834 }
835
836 if (mouse.motion == Mouse::Pressed) {
837 selection_pending_ = CaptureMouse();
838 selection_data_.start_x = mouse.x;
839 selection_data_.start_y = mouse.y;
840 selection_data_.end_x = mouse.x;
841 selection_data_.end_y = mouse.y;
842 return false;
843 }
844
845 if (!selection_pending_) {
846 return false;
847 }
848
849 if (mouse.motion == Mouse::Moved) {
850 if ((mouse.x != selection_data_.end_x) ||
851 (mouse.y != selection_data_.end_y)) {
852 selection_data_.end_x = mouse.x;
853 selection_data_.end_y = mouse.y;
854 selection_data_.empty = false;
855 }
856
857 return true;
858 }
859
860 if (mouse.motion == Mouse::Released) {
861 selection_pending_ = nullptr;
862 selection_data_.end_x = mouse.x;
863 selection_data_.end_y = mouse.y;
864 selection_data_.empty = false;
865 return true;
866 }
867
868 return false;
869}
870
871// private
872// NOLINTNEXTLINE
873void ScreenInteractive::Draw(Component component) {
874 if (frame_valid_) {
875 return;
876 }
877 auto document = component->Render();
878 int dimx = 0;
879 int dimy = 0;
880 auto terminal = Terminal::Size();
881 document->ComputeRequirement();
882 switch (dimension_) {
883 case Dimension::Fixed:
884 dimx = dimx_;
885 dimy = dimy_;
886 break;
887 case Dimension::TerminalOutput:
888 dimx = terminal.dimx;
889 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
890 break;
891 case Dimension::Fullscreen:
892 dimx = terminal.dimx;
893 dimy = terminal.dimy;
894 break;
895 case Dimension::FitComponent:
896 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
897 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
898 break;
899 }
900
901 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
902 ResetCursorPosition();
903 std::cout << ResetPosition(/*clear=*/resized);
904
905 // ターミナルの幅が減少すると、ターミナルエミュレータは行を折り返し始め、ディスプレイを汚します。完全にクリアする必要があります。
906 if ((dimx < dimx_) && !use_alternative_screen_) {
907 std::cout << "\033[J"; // clear terminal output
908 std::cout << "\033[H"; // move cursor to home position
909 }
910
911 // 必要に応じて画面のサイズを変更します。
912 if (resized) {
913 dimx_ = dimx;
914 dimy_ = dimy;
915 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
916 cursor_.x = dimx_ - 1;
917 cursor_.y = dimy_ - 1;
918 }
919
920 // ターミナルエミュレータに、画面に対するフレームの位置を定期的に要求します。これは、画面座標で報告されたマウス位置をフレーム座標に変換するのに役立ちます。
921#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
922 // Microsoftのターミナルには[バグ]があります。カーソル位置を報告する際、複数の出力シーケンスが混在してゴミになります。
923 // これにより、FTXUIユーザーはInputコンポーネントに「1;1;R」シーケンスが表示されることになります。詳細は[issue]を参照してください。解決策は、カーソル位置の要求頻度を減らすことです。[bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
924 // https://github.com/ArthurSonzogni/FTXUI/issues/136
925 static int i = -3;
926 ++i;
927 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
928 std::cout << DeviceStatusReport(DSRMode::kCursor);
929 }
930#else
931 static int i = -3;
932 ++i;
933 if (!use_alternative_screen_ &&
934 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
935 std::cout << DeviceStatusReport(DSRMode::kCursor);
936 }
937#endif
938 previous_frame_resized_ = resized;
939
940 selection_ = selection_data_.empty
941 ? std::make_unique<Selection>()
942 : std::make_unique<Selection>(
943 selection_data_.start_x, selection_data_.start_y, //
944 selection_data_.end_x, selection_data_.end_y);
945 Render(*this, document.get(), *selection_);
946
947 // CJK文字を挿入するツールを使用しているユーザーのためにカーソル位置を設定します。
948 {
949 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
950 const int dy = dimy_ - 1 - cursor_.y;
951
952 set_cursor_position.clear();
953 reset_cursor_position.clear();
954
955 if (dy != 0) {
956 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
957 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
958 }
959
960 if (dx != 0) {
961 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
962 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
963 }
964
965 if (cursor_.shape == Cursor::Hidden) {
966 set_cursor_position += "\033[?25l";
967 } else {
968 set_cursor_position += "\033[?25h";
969 set_cursor_position +=
970 "\033[" + std::to_string(int(cursor_.shape)) + " q";
971 }
972 }
973
974 std::cout << ToString() << set_cursor_position;
975 Flush();
976 Clear();
977 frame_valid_ = true;
978 frame_count_++;
979}
980
981// private
982void ScreenInteractive::ResetCursorPosition() {
983 std::cout << reset_cursor_position;
984 reset_cursor_position = "";
985}
986
987/// @brief メインループを終了する関数を返します。
989 return [this] { Exit(); };
990}
991
992/// @brief メインループを終了します。
994 Post([this] { ExitNow(); });
995}
996
997// private:
998void ScreenInteractive::ExitNow() {
999 quit_ = true;
1000}
1001
1002// private:
1003void ScreenInteractive::Signal(int signal) {
1004 if (signal == SIGABRT) {
1005 Exit();
1006 return;
1007 }
1008
1009// Windows は SIGTSTP / SIGWINCH をサポートしていません
1010#if !defined(_WIN32)
1011 if (signal == SIGTSTP) {
1012 Post([&] {
1013 ResetCursorPosition();
1014 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1015 Uninstall();
1016 dimx_ = 0;
1017 dimy_ = 0;
1018 Flush();
1019 std::ignore = std::raise(SIGTSTP);
1020 Install();
1021 });
1022 return;
1023 }
1024
1025 if (signal == SIGWINCH) {
1026 Post(Event::Special({0}));
1027 return;
1028 }
1029#endif
1030}
1031
1032void ScreenInteractive::FetchTerminalEvents() {
1033#if defined(_WIN32)
1034 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1035 // コンソールに入力があるか確認します。
1036 auto console = GetStdHandle(STD_INPUT_HANDLE);
1037 DWORD number_of_events = 0;
1038 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1039 return std::vector<INPUT_RECORD>();
1040 }
1041 if (number_of_events <= 0) {
1042 // 入力がないため、戻ります。
1043 return std::vector<INPUT_RECORD>();
1044 }
1045 // 入力イベントを読み取ります。
1046 std::vector<INPUT_RECORD> records(number_of_events);
1047 DWORD number_of_events_read = 0;
1048 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1049 &number_of_events_read)) {
1050 return std::vector<INPUT_RECORD>();
1051 }
1052 records.resize(number_of_events_read);
1053 return records;
1054 };
1055
1056 auto records = get_input_records();
1057 if (records.size() == 0) {
1058 const auto timeout =
1059 std::chrono::steady_clock::now() - internal_->last_char_time;
1060 const size_t timeout_microseconds =
1061 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1062 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1063 return;
1064 }
1065 internal_->last_char_time = std::chrono::steady_clock::now();
1066
1067 // 入力イベントをFTXUIイベントに変換します。
1068 // 各イベントに対して、ターミナル入力パーサーを呼び出してEventに変換します。
1069 for (const auto& r : records) {
1070 switch (r.EventType) {
1071 case KEY_EVENT: {
1072 auto key_event = r.Event.KeyEvent;
1073 // UPキーイベントを無視します
1074 if (key_event.bKeyDown == FALSE) {
1075 continue;
1076 }
1077 std::wstring wstring;
1078 wstring += key_event.uChar.UnicodeChar;
1079 for (auto it : to_string(wstring)) {
1080 internal_->terminal_input_parser.Add(it);
1081 }
1082 } break;
1083 case WINDOW_BUFFER_SIZE_EVENT:
1084 Post(Event::Special({0}));
1085 break;
1086 case MENU_EVENT:
1087 case FOCUS_EVENT:
1088 case MOUSE_EVENT:
1089 // TODO(mauve): 後で実装します。
1090 break;
1091 }
1092 }
1093#elif defined(__EMSCRIPTEN__)
1094 // ターミナルから文字を読み取ります。
1095 // 非ブロッキングになるように設定しました。
1096 std::array<char, 128> out{};
1097 size_t l = read(STDIN_FILENO, out.data(), out.size());
1098 if (l == 0) {
1099 const auto timeout =
1100 std::chrono::steady_clock::now() - internal_->last_char_time;
1101 const size_t timeout_microseconds =
1102 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1103 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1104 return;
1105 }
1106 // 文字をイベントに変換します。
1107 for (size_t i = 0; i < l; ++i) {
1108 internal_->terminal_input_parser.Add(out[i]);
1109 }
1110#else // POSIX (Linux & Mac)
1111 if (!CheckStdinReady(tty_fd_)) {
1112 const auto timeout =
1113 std::chrono::steady_clock::now() - internal_->last_char_time;
1114 const size_t timeout_ms =
1115 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1116 internal_->terminal_input_parser.Timeout(timeout_ms);
1117 return;
1118 }
1119 internal_->last_char_time = std::chrono::steady_clock::now();
1120
1121 // ターミナルから文字を読み取ります。
1122 std::array<char, 128> out{};
1123 size_t l = read(tty_fd_, out.data(), out.size());
1124
1125 // 文字をイベントに変換します。
1126 for (size_t i = 0; i < l; ++i) {
1127 internal_->terminal_input_parser.Add(out[i]);
1128 }
1129#endif
1130}
1131
1132void ScreenInteractive::PostAnimationTask() {
1133 Post(AnimationTask());
1134
1135 // アニメーションタスクを15msごとに繰り返します。これは約66fpsのフレームレートに相当します。
1136 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1137 std::chrono::milliseconds(15));
1138}
1139
1140bool ScreenInteractive::SelectionData::operator==(
1141 const ScreenInteractive::SelectionData& other) const {
1142 if (empty && other.empty) {
1143 return true;
1144 }
1145 if (empty || other.empty) {
1146 return false;
1147 }
1148 return start_x == other.start_x && start_y == other.start_y &&
1149 end_x == other.end_x && end_y == other.end_y;
1150}
1151
1152bool ScreenInteractive::SelectionData::operator!=(
1153 const ScreenInteractive::SelectionData& other) const {
1154 return !(*this == other);
1155}
1156
1157} // namespace ftxui.
static void Signal(ScreenInteractive &s, int signal)
auto PostTask(Task task) -> void
タスクを即座に実行するようにスケジュールします。
auto RunUntilIdle() -> std::optional< std::chrono::steady_clock::duration >
キュー内のタスクを実行し、次の遅延タスクが実行されるまでの遅延を返します。
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
特定の期間の後に実行されるようにタスクをスケジュールします。
size_t ExecutedTasks() const
static const Event CtrlC
Definition event.hpp:70
static ScreenInteractive TerminalOutput()
ターミナル出力の幅に一致し、描画されるコンポーネントの高さに一致するScreenInteractiveを作成します。
void HandlePipedInput(bool enable=true)
自動パイプ入力処理を有効または無効にします。 有効にすると、FTXUIはパイプ入力を検出し、キーボード入力のためにstdinを/dev/ttyからリダイレクトし、アプリケーションがパイプデータを読み取り...
void Exit()
メインループを終了します。
static const Event CtrlZ
Definition event.hpp:93
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
メインループにイベントを追加します。 これは、他のすべてのスケジュールされたイベントの後に実行されます。
void Post(Task task)
メインループにタスクを追加します。 これは、他のすべてのスケジュールされたタスクの後に実行されます。
static ScreenInteractive FitComponent()
描画されるコンポーネントの幅と高さに一致するScreenInteractiveを作成します。
static ScreenInteractive Fullscreen()
static const Event Custom
Definition event.hpp:96
static ScreenInteractive FullscreenPrimaryScreen()
ターミナルサイズの全体を使用するScreenInteractiveを作成します。プライマリスクリーンバッファが使用されます。これは、ターミナルがリサイズされた場合、以前のコンテンツがターミナルコンテンツ...
static ScreenInteractive * Active()
現在アクティブな画面を返します。アクティブな画面がない場合はヌルを返します。
CapturedMouse CaptureMouse()
マウスをキャプチャできることに関するユニークロックを取得しようとします。
std::string GetSelection()
現在の選択内容を返します
static ScreenInteractive FullscreenAlternateScreen()
ターミナルサイズの全体を使用するScreenInteractiveを作成します。これは、ターミナルコンテンツを乱すことを避けるために、代替スクリーンバッファを使用します。
void TrackMouse(bool enable=true)
マウスが追跡され、イベントが報告されるかどうかを設定します。 メインループの外側で呼び出されます。例: ScreenInteractive::Loop(...)。
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
すべてのアニメーションが完了するまで、画面をもう一度描画するタスクを追加します。
Closure ExitLoopClosure()
メインループを終了する関数を返します。
void ForceHandleCtrlC(bool force)
コンポーネントがEvent::CtrlCをキャッチした場合でも、FTXUIにCtrl-Cを処理させるか処理させないかを強制します。
void ForceHandleCtrlZ(bool force)
コンポーネントがEvent::CtrlZをキャッチした場合でも、FTXUIにCtrl-Zを処理させるか処理させないかを強制します。
Closure WithRestoredIO(Closure)
関数を装飾します。それは同じように実行されますが、実行中に現在アクティブなスクリーンターミナルフックは一時的にアンインストールされます。
static Event Special(std::string)
ライブラリのユーザーによって意味が定義されるカスタムイベント。
Definition event.cpp:73
Loopは、コンポーネントのイベントループを管理するクラスです。
Definition loop.hpp:53
ScreenInteractive はイベントを処理し、メインループを実行し、コンポーネントを管理できる Screen です。
void RequestAnimationFrame()
RequestAnimationFrameは、次のアニメーションサイクルで新しいフレームが描画されるよう要求する関数です。
イベントを表します。キープレスイベント、ターミナルのリサイズなど、さまざまなイベントがあります。
Definition event.hpp:28
void Render(Screen &screen, const Element &element)
要素をftxui::Screenに表示します。
Definition node.cpp:84
int dimy() const
Definition image.hpp:36
std::string ToString() const
Definition screen.cpp:410
std::string ResetPosition(bool clear=false) const
カーソル位置を画面の先頭にリセットするために出力する文字列を返します。
Definition screen.cpp:468
Cursor cursor_
Definition screen.hpp:76
void Clear()
画面からすべてのピクセルをクリアします。
Definition screen.cpp:487
int dimx() const
Definition image.hpp:35
std::vector< std::vector< Pixel > > pixels_
Definition image.hpp:46
Dimensions Size()
ターミナルサイズを取得します。
Definition terminal.cpp:87
FTXUI ftxui::Dimension::名前空間
FTXUI ftxui::animation::名前空間
void SetFallbackSize(const Dimensions &fallbackSize)
自動検出が失敗した場合にターミナルサイズを上書きします
Definition terminal.cpp:114
std::chrono::duration< float > Duration
Definition animation.hpp:24
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:23
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
FTXUI ftxui:: 名前空間
Definition animation.hpp:9
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(const std::wstring &s)
Element select(Element e)
子要素を兄弟要素の中でフォーカスされたものとして設定します。
Definition frame.cpp:108
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:13
std::function< void()> Closure
Definition task.hpp:12
std::shared_ptr< ComponentBase > Component