FTXUI 7.0.3
C++ functional terminal UI.
Loading...
Searching...
No Matches
app.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
4#include <algorithm> // for any_of, 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
12#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
13#include <ftxui/screen/screen.hpp> // for Cell, Screen::Cursor, Screen, Screen::Cursor::Hidden
14#include <functional> // for function
15#include <initializer_list> // for initializer_list
16#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
17#include <map>
18#include <memory>
19#include <stack> // for stack
20#include <string>
21#include <string_view>
22#include <thread> // for thread, sleep_for
23#include <tuple> // for _Swallow_assign, ignore
24#include <type_traits>
25#include <utility> // for move, swap
26#include <variant> // for visit, variant
27#include <vector> // for vector
28
29#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
30#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
31#include "ftxui/component/component_base.hpp" // for ComponentBase
32#include "ftxui/component/event.hpp" // for Event
33#include "ftxui/component/loop.hpp" // for Loop
36#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
37#include "ftxui/dom/node.hpp" // for Node, Render
38#include "ftxui/screen/cell.hpp" // for Cell
39#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
40#include "ftxui/screen/util.hpp" // for util::clamp
41#include "ftxui/util/autoreset.hpp" // for AutoReset
42
43#if defined(_WIN32)
44#define DEFINE_CONSOLEV2_PROPERTIES
45#define WIN32_LEAN_AND_MEAN
46#ifndef NOMINMAX
47#define NOMINMAX
48#endif
49#include <io.h>
50#include <windows.h>
51#else
52#include <fcntl.h>
53#include <poll.h>
54#include <sys/poll.h>
55#include <sys/types.h>
56#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
57#include <unistd.h> // for STDIN_FILENO, STDOUT_FILENO, read
58#endif
59
60#if defined(__EMSCRIPTEN__)
61#include <emscripten.h>
62#endif
63
64namespace ftxui {
65
66enum class AppDimension {
68 Fixed,
71};
72
73namespace animation {
75 auto* screen = App::Active();
76 if (screen) {
77 screen->RequestAnimationFrame();
78 }
79}
80} // namespace animation
81
82#if defined(__EMSCRIPTEN__)
83extern "C" {
84EMSCRIPTEN_KEEPALIVE
85void ftxui_on_resize(int columns, int rows) {
87 columns,
88 rows,
89 });
90 std::raise(SIGWINCH);
91}
92}
93#endif
94
95struct App::Internal {
96 App* public_;
97
98 App* suspended_screen_ = nullptr;
99 const AppDimension dimension_;
100 const bool use_alternative_screen_;
101
102 bool track_mouse_ = true;
103
104 std::string set_cursor_position_;
105 std::string reset_cursor_position_;
106
107 std::atomic<bool> quit_{false};
108 bool installed_ = false;
109 bool animation_requested_ = false;
110 animation::TimePoint previous_animation_time_;
111
112 int cursor_x_ = 1;
113 int cursor_y_ = 1;
114
115 std::uint64_t frame_count_ = 0;
116 bool mouse_captured = false;
117 bool previous_frame_resized_ = false;
118
119 bool frame_valid_ = false;
120
121 bool force_handle_ctrl_c_ = true;
122 bool force_handle_ctrl_z_ = true;
123
124 int cursor_reset_shape_ = 1;
125
126 // パイプ入力処理の状態 (POSIX のみ)
127 bool handle_piped_input_ = true;
128 bool is_stdin_a_tty_ = false;
129 bool is_stdout_a_tty_ = false;
130 // /dev/tty のファイル記述子。パイプ入力処理に使用されます。
131 int tty_fd_ = -1;
132
133 std::string terminal_name_ = "unknown";
134 int terminal_version_ = 0;
135
136 std::string terminal_emulator_name_ = "unknown";
137 std::string terminal_emulator_version_ = "unknown";
138
139 std::vector<int> terminal_capabilities_;
140
141 // 選択 API:
142 CapturedMouse selection_pending_;
143 struct SelectionData {
144 int start_x = -1;
145 int start_y = -1;
146 int end_x = -2;
147 int end_y = -2;
148 bool empty = true;
149 bool operator==(const SelectionData& other) const {
150 if (empty && other.empty) {
151 return true;
152 }
153 if (empty || other.empty) {
154 return false;
155 }
156 return start_x == other.start_x && start_y == other.start_y &&
157 end_x == other.end_x && end_y == other.end_y;
158 }
159 bool operator!=(const SelectionData& other) const {
160 return !(*this == other);
161 }
162 };
163 SelectionData selection_data_;
164 SelectionData selection_data_previous_;
165 std::unique_ptr<Selection> selection_;
166 std::function<void()> selection_on_change_;
167
168 Component component_;
169
170 // Internalに事前に存在:
171 TerminalInputParser terminal_input_parser;
172 task::TaskRunner task_runner;
173 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
174 std::chrono::steady_clock::now();
175 std::string output_buffer;
176
177 class ThrottledRequest {
178 public:
179 ThrottledRequest(App::Internal* internal, std::function<void()> send)
180 : internal_(internal), send_(std::move(send)) {}
181
182 void Request(bool force = false) {
183 if (!internal_->is_stdin_a_tty_) {
184 return;
185 }
186
187 if (force) {
188 Send();
189 return;
190 }
191
192 // 一度に1つの保留中のリクエストのみを許可します。これはターミナルを
193 // リクエストで溢れさせないようにするためです。
194 if (HasPending()) {
195 return;
196 }
197
198 const auto now = std::chrono::steady_clock::now();
199 if (now - last_request_time_ < std::chrono::milliseconds(500)) {
200 // 最後のリクエストから経過時間が短すぎます。スキップします:
201 // リクエストは、カーソルがフレームの原点に移動された直後に、
202 // Draw()から同期的に送信される必要があります。これにより、
203 // ターミナルの応答がその位置を反映します。Draw()は次のフレームで
204 // Request()を再度呼び出すため、リクエストは失われるのではなく、
205 // 遅延されるだけです。
206 return;
207 }
208
209 Send();
210 }
211
212 void OnReply() { pending_request_ = false; }
213
214 bool HasPending() const {
215 if (!pending_request_) {
216 return false;
217 }
218 const auto now = std::chrono::steady_clock::now();
219 return now - last_sent_time_ < std::chrono::seconds(5);
220 }
221
222 private:
223 void Send() {
224 last_sent_time_ = std::chrono::steady_clock::now();
225 last_request_time_ = last_sent_time_;
226 pending_request_ = true;
227 send_();
228 }
229
230 App::Internal* internal_;
231 std::function<void()> send_;
232 bool pending_request_ = false;
233 std::chrono::steady_clock::time_point last_request_time_ =
234 std::chrono::steady_clock::now() - std::chrono::hours(1);
235 std::chrono::steady_clock::time_point last_sent_time_ =
236 std::chrono::steady_clock::now() - std::chrono::hours(1);
237 };
238
239 ThrottledRequest cursor_position_request;
240
241 MultiReceiverBuffer<Event> event_buffer;
242 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
243
244 Internal(App* app, AppDimension dimension, bool use_alternative_screen);
245
246 void ExitNow();
247 void Install();
248 void Uninstall();
249 void PreMain();
250 void PostMain();
251 bool HasQuitted();
252 void RunOnce(const Component& component);
253 void RunOnceBlocking(Component component);
254 void HandleTask(Component component, Task& task);
255 bool HandleSelection(bool handled, Event event);
256 void Draw(Component component);
257 std::string ResetCursorPosition();
258 void RequestCursorPosition(bool force = false);
259 void TerminalSend(std::string_view);
260 void TerminalFlush();
261 void InstallPipedInputHandling();
262 void InstallTerminalInfo();
263 void Signal(int signal);
264 size_t FetchTerminalEvents();
265 void PostAnimationTask();
266};
267
268namespace {
269
270App* g_active_screen = nullptr; // NOLINT
271
272std::stack<Closure> on_exit_functions; // NOLINT
273
274void OnExit() {
275 while (!on_exit_functions.empty()) {
276 on_exit_functions.top()();
277 on_exit_functions.pop();
278 }
279}
280
281// CSI: Control Sequence Introducer
282const std::string CSI = "\x1b["; // NOLINT
283 //
284// DCS: Device Control String
285const std::string DCS = "\x1bP"; // NOLINT
286
287// ST: String Terminator
288const std::string ST = "\x1b\\"; // NOLINT
289
290// DECRQSS: Request Status String
291// DECSCUSR: Set Cursor Style
292const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
293
294// DEC: Digital Equipment Corporation
295enum class DECMode : std::uint16_t {
296 kLineWrap = 7,
297 kCursor = 25,
298
299 kMouseX10 = 9,
300 kMouseVt200 = 1000,
301 kMouseVt200Highlight = 1001,
302
303 kMouseBtnEventMouse = 1002,
304 kMouseAnyEvent = 1003,
305
306 kMouseUtf8 = 1005,
307 kMouseSgrExtMode = 1006,
308 kMouseUrxvtMode = 1015,
309 kMouseSgrPixelsMode = 1016,
310 kAlternateScreen = 1049,
311};
312
313// Device Status Report (DSR) {
314enum class DSRMode : std::uint8_t {
315 kCursor = 6,
316};
317
318std::string Serialize(const std::vector<DECMode>& parameters) {
319 bool first = true;
320 std::string out;
321 for (const DECMode parameter : parameters) {
322 if (!first) {
323 out += ";";
324 }
325 out += std::to_string(int(parameter));
326 first = false;
327 }
328 return out;
329}
330
331// DEC Private Mode Set (DECSET)
332std::string Set(const std::vector<DECMode>& parameters) {
333 return CSI + "?" + Serialize(parameters) + "h";
334}
335
336// DEC Private Mode Reset (DECRST)
337std::string Reset(const std::vector<DECMode>& parameters) {
338 return CSI + "?" + Serialize(parameters) + "l";
339}
340
341// Device Status Report (DSR)
342std::string DeviceStatusReport(DSRMode ps) {
343 return CSI + std::to_string(int(ps)) + "n";
344}
345
346class CapturedMouseImpl : public CapturedMouseInterface {
347 public:
348 explicit CapturedMouseImpl(std::function<void(void)> callback)
349 : callback_(std::move(callback)) {}
350 ~CapturedMouseImpl() override { callback_(); }
351 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
352 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
353 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
354 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
355
356 private:
357 std::function<void(void)> callback_;
358};
359
360#if !defined(_WIN32)
361std::atomic<int> g_signal_exit_count = 0; // NOLINT
362std::atomic<int> g_signal_stop_count = 0; // NOLINT
363std::atomic<int> g_signal_resize_count = 0; // NOLINT
364#else
365std::atomic<int> g_signal_exit_count = 0; // NOLINT
366#endif
367
368// ターミナルが現在rawモードに設定されているかどうかを追跡します。
369// 緊急時と通常の終了時における二重の復元を防ぐために使用されます。
370std::atomic<bool> g_terminal_is_raw{false};
371
372// 最後に受信した遅延シグナル(SIGINT、SIGTERMなど)を保存し、
373// アンインストール/終了時に再発生させます。
374std::atomic<int> g_last_signal{0}; // NOLINT
375
376#if defined(_WIN32)
377using SignalHandler = void (*)(int);
378// FTXUIが独自のハンドラをインストールする前の元のシグナルハンドラを
379// 保存します。
380std::map<int, SignalHandler> g_old_signal_handlers;
381
382// 終了時に復元するために、元のコンソールモードを保存します。
383DWORD g_original_stdout_mode = 0;
384DWORD g_original_stdin_mode = 0;
385bool g_has_original_console_mode = false;
386#else
387// FTXUIが独自のハンドラをインストールする前の元のsigaction構造体を
388// 保存します。
389std::map<int, struct sigaction> g_old_sigactions;
390
391// 終了時に復元するために、元のtermiosターミナル設定を保存します。
392struct termios g_original_termios;
393bool g_has_original_termios = false;
394int g_tty_fd = -1;
395#endif
396
397// 指定されたシグナルの元のシグナルハンドラを復元し、再発生させます。
398// 非同期シグナルセーフな関数です。
399void RestoreSignalHandlerAndRaise(int signal) {
400#if defined(_WIN32)
401 auto it = g_old_signal_handlers.find(signal);
402 auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
403 std::signal(signal, old_handler);
404#else
405 auto it = g_old_sigactions.find(signal);
406 if (it != g_old_sigactions.end()) {
407 sigaction(signal, &it->second, nullptr);
408 } else {
409 struct sigaction sa;
410 sa.sa_handler = SIG_DFL;
411 sigemptyset(&sa.sa_mask);
412 sa.sa_flags = 0;
413 sigaction(signal, &sa, nullptr);
414 }
415#endif
416 std::raise(signal);
417}
418
419// 緊急時のターミナル状態の復元。
420// 非同期シグナルセーフな関数です。
421void RestoreTerminalEmergency() {
422 if (!g_terminal_is_raw.exchange(false)) {
423 return;
424 }
425#if defined(_WIN32)
426 if (g_has_original_console_mode) {
427 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
428 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
429 SetConsoleMode(stdout_handle, g_original_stdout_mode);
430 SetConsoleMode(stdin_handle, g_original_stdin_mode);
431 }
432#else
433 if (g_has_original_termios && g_tty_fd >= 0) {
434 const char restore_seq[] =
435 "\x1b[?25h" // カーソルを表示します。
436 "\x1b[?1049l" // 通常の画面バッファに切り替えます。
437 "\x1b[?1000l" // 通常のマウストラッキングを無効にします。
438 "\x1b[?1002l" // ボタンイベントマウストラッキングを無効にします。
439 "\x1b[?1003l" // すべてのモーションマウストラッキングを無効にします。
440 "\x1b[?1006l" // SGRマウストラッキングを無効にします。
441 "\x1b[?1015l" // Urxvtマウストラッキングを無効にします。
442 "\x1b[?7h"; // 行の折り返しを有効にします。
443 std::ignore = write(STDOUT_FILENO, restore_seq, sizeof(restore_seq) - 1);
444 tcsetattr(g_tty_fd, TCSANOW, &g_original_termios);
445 }
446#endif
447}
448
449// 非同期シグナルセーフな関数
450void RecordSignal(int signal) {
451 switch (signal) {
452 // 異常終了 (例: abort()やアサーション失敗)。
453 case SIGABRT:
454 // 誤った算術演算 (例: ゼロ除算)。
455 case SIGFPE:
456 // 不正な命令。
457 case SIGILL:
458 // 無効なメモリ参照 (セグメンテーション違反)。
459 case SIGSEGV:
460#if !defined(_WIN32)
461 // バスエラー (例: 不正なメモリアクセスアライメント)。
462 case SIGBUS:
463 // 不正なシステムコール。
464 case SIGSYS:
465#endif
466 {
467 RestoreTerminalEmergency();
468 RestoreSignalHandlerAndRaise(signal);
469 break;
470 }
471
472 // ターミナル割り込み (例: Ctrl-C)。
473 case SIGINT:
474 // 終了リクエスト。
475 case SIGTERM:
476#if !defined(_WIN32)
477 // ターミナル終了 (例: Ctrl-\、コアダンプを生成)。
478 case SIGQUIT:
479 // 制御ターミナルでのハングアップ検出、または制御プロセスの終了。
480 case SIGHUP:
481#endif
482 g_last_signal.store(signal);
483 g_signal_exit_count++;
484 break;
485
486#if !defined(_WIN32)
487 // ターミナル停止シグナル (例: Ctrl-Z)。
488 case SIGTSTP: // NOLINT
489 g_signal_stop_count++;
490 break;
491
492 // ターミナルウィンドウサイズの変更。
493 case SIGWINCH: // NOLINT
494 g_signal_resize_count++;
495 break;
496#endif
497
498 default:
499 break;
500 }
501}
502
503void ExecuteSignalHandlers() {
504 if (g_last_signal.load() != 0) {
505 App::Private::Signal(*g_active_screen, SIGABRT);
506 }
507
508 int signal_exit_count = g_signal_exit_count.exchange(0);
509 while (signal_exit_count--) {
510 App::Private::Signal(*g_active_screen, SIGABRT);
511 }
512
513#if !defined(_WIN32)
514 int signal_stop_count = g_signal_stop_count.exchange(0);
515 while (signal_stop_count--) {
516 App::Private::Signal(*g_active_screen, SIGTSTP);
517 }
518
519 int signal_resize_count = g_signal_resize_count.exchange(0);
520 while (signal_resize_count--) {
521 App::Private::Signal(*g_active_screen, SIGWINCH);
522 }
523#endif
524}
525
526void InstallSignalHandler(int sig) {
527#if defined(_WIN32)
528 auto old_signal_handler = std::signal(sig, RecordSignal);
529 g_old_signal_handlers[sig] = old_signal_handler;
530 on_exit_functions.emplace(
531 [=] { std::ignore = std::signal(sig, old_signal_handler); });
532#else
533 struct sigaction sa;
534 sa.sa_handler = RecordSignal;
535 sigemptyset(&sa.sa_mask);
536 sa.sa_flags = SA_RESTART;
537 struct sigaction old_sa;
538 sigaction(sig, &sa, &old_sa);
539 g_old_sigactions[sig] = old_sa;
540 on_exit_functions.emplace([=] { sigaction(sig, &old_sa, nullptr); });
541#endif
542}
543
544} // namespace
545
546App::Internal::Internal(App* app,
547 AppDimension dimension,
548 bool use_alternative_screen)
549 : public_(app),
550 dimension_(dimension),
551 use_alternative_screen_(use_alternative_screen),
552 terminal_input_parser([&](Event event) {
553 event_buffer.Push(std::move(event));
554 }),
555 cursor_position_request(this, [this] {
556 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
557 }) {
558 main_loop_receiver = event_buffer.CreateReceiver();
559}
560
561void App::Internal::ExitNow() {
562 quit_ = true;
563}
564
565void App::Internal::Install() {
566 frame_valid_ = false;
567
568 // ターミナルの設定を変更し始める前に、ユーザーがそれまでに出力した内容が
569 // 完全に反映されるよう、stdoutのバッファをフラッシュします。これは
570 // 重要です。なぜなら、以下でターミナルエミュレータと通信するために
571 // 2つの異なるチャネル(stdout と termios/WinAPI)を使用しているからです。以下を参照:
572 // https://github.com/ArthurSonzogni/FTXUI/issues/846
573 TerminalFlush();
574
575 InstallPipedInputHandling();
576
577 // 新しい設定をアンインストールした後、それを端末に完全に適用するために
578 // フラッシュします:
579 on_exit_functions.emplace([this] { TerminalFlush(); });
580
581 // 終了時に端末の状態を復元するためにシグナルハンドラをインストールします。デフォルトの
582 // シグナルハンドラは終了時に復元されます。
583 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
584 InstallSignalHandler(signal);
585 }
586
587// 古い端末設定を保存し、終了時に復元します。
588#if defined(_WIN32)
589 // stdoutおよびstdinでVT処理を有効にする
590 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
591 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
592
593 DWORD out_mode = 0;
594 DWORD in_mode = 0;
595 GetConsoleMode(stdout_handle, &out_mode);
596 GetConsoleMode(stdin_handle, &in_mode);
597 g_original_stdout_mode = out_mode;
598 g_original_stdin_mode = in_mode;
599 g_has_original_console_mode = true;
600 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
601 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
602
603 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
604 const int enable_virtual_terminal_processing = 0x0004;
605 const int disable_newline_auto_return = 0x0008;
606 out_mode |= enable_virtual_terminal_processing;
607 out_mode |= disable_newline_auto_return;
608
609 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
610 const int enable_line_input = 0x0002;
611 const int enable_echo_input = 0x0004;
612 const int enable_virtual_terminal_input = 0x0200;
613 const int enable_window_input = 0x0008;
614 in_mode &= ~enable_echo_input;
615 in_mode &= ~enable_line_input;
616 in_mode |= enable_virtual_terminal_input;
617 in_mode |= enable_window_input;
618
619 SetConsoleMode(stdin_handle, in_mode);
620 SetConsoleMode(stdout_handle, out_mode);
621#else // POSIX (Linux & Mac)
622 for (const int signal :
623 {SIGWINCH, SIGTSTP, SIGBUS, SIGSYS, SIGQUIT, SIGHUP}) {
624 InstallSignalHandler(signal);
625 }
626
627 struct termios terminal; // NOLINT
628 tcgetattr(tty_fd_, &terminal);
629 g_original_termios = terminal;
630 g_tty_fd = tty_fd_;
631 g_has_original_termios = true;
632 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
633 tcsetattr(tty_fd_, TCSANOW, &terminal);
634 });
635
636 // rawターミナル入力モードを有効にする
637 terminal.c_iflag &= ~IGNBRK; // ブレーク条件の無視を無効にする
638 terminal.c_iflag &= ~BRKINT; // ブレークによる入出力のフラッシュを
639 // 無効にする
640 terminal.c_iflag &= ~PARMRK; // パリティエラーのマーキングを無効にする。
641 terminal.c_iflag &= ~ISTRIP; // 文字の8ビット目のストリッピングを無効にする。
642 terminal.c_iflag &= ~INLCR; // NLからCRへのマッピングを無効にする。
643 terminal.c_iflag &= ~IGNCR; // CRの無視を無効にする。
644 terminal.c_iflag &= ~ICRNL; // CRからNLへのマッピングを無効にする。
645 terminal.c_iflag &= ~IXON; // 出力でのXON/XOFFフロー制御を無効にする
646
647 terminal.c_lflag &= ~ECHO; // 入力文字のエコーを無効にする。
648 terminal.c_lflag &= ~ECHONL; // 改行文字のエコーを無効にする。
649 terminal.c_lflag &= ~ICANON; // Canonicalモードを無効にする。
650 terminal.c_lflag &= ~ISIG; // 以下のキーを押したときにシグナルを送信するのを無効にする:
651 // - => DSUSP
652 // - C-Z => SUSP
653 // - C-C => INTR
654 // - C-d => QUIT
655 terminal.c_lflag &= ~IEXTEN; // 拡張入力処理を無効にする
656 terminal.c_cflag |= CS8; // 1バイトあたり8ビット
657
658 terminal.c_cc[VMIN] = 0; // 非canonical
659 // 読み取りのための最小文字数。
660 terminal.c_cc[VTIME] = 0; // 非canonical読み取りのためのデシ秒単位のタイムアウト。
661
662 tcsetattr(tty_fd_, TCSANOW, &terminal);
663
664#endif
665
666 auto enable = [&](const std::vector<DECMode>& parameters) {
667 TerminalSend(Set(parameters));
668 on_exit_functions.emplace(
669 [this, parameters] { TerminalSend(Reset(parameters)); });
670 };
671
672 auto disable = [&](const std::vector<DECMode>& parameters) {
673 TerminalSend(Reset(parameters));
674 on_exit_functions.emplace(
675 [this, parameters] { TerminalSend(Set(parameters)); });
676 };
677
678 if (use_alternative_screen_) {
679 enable({
680 DECMode::kAlternateScreen,
681 });
682 }
683
684 disable({
685 DECMode::kLineWrap,
686 });
687
688 if (track_mouse_) {
689 enable({DECMode::kMouseVt200});
690 enable({DECMode::kMouseAnyEvent});
691 enable({DECMode::kMouseUrxvtMode});
692 enable({DECMode::kMouseSgrExtMode});
693 }
694
695 // 新しい設定をインストールした後、それを端末に完全に適用するために
696 // フラッシュします:
697 TerminalFlush();
698
699 InstallTerminalInfo();
700
701 quit_ = false;
702
703 PostAnimationTask();
704
705 installed_ = true;
706 g_terminal_is_raw = true;
707}
708
709void App::Internal::Uninstall() {
710 g_terminal_is_raw = false;
711 installed_ = false;
712
713 // シャットダウン中、すべての返信を待ちます。
714 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
715 auto closing_receiver =
716 event_buffer.CreateReceiverAt(main_loop_receiver->index());
717 auto start = std::chrono::steady_clock::now();
718 while (cursor_position_request.HasPending()) {
719 FetchTerminalEvents();
720
721 while (closing_receiver->Has()) {
722 const auto event = closing_receiver->Pop();
723 if (event.is_cursor_position()) {
724 cursor_x_ = event.cursor_x();
725 cursor_y_ = event.cursor_y();
726 cursor_position_request.OnReply();
727 }
728 }
729
730 task_runner.RunUntilIdle();
731
732 if (std::chrono::steady_clock::now() - start >
733 std::chrono::milliseconds(400)) {
734 break;
735 }
736 std::this_thread::sleep_for(std::chrono::milliseconds(10));
737 }
738 }
739
740 OnExit();
741}
742
743void App::Internal::PreMain() {
744 // 以前アクティブだったスクリーンを一時停止:
745 if (g_active_screen) {
746 std::swap(suspended_screen_, g_active_screen);
747 // カーソル位置をスクリーンの先頭にリセットし、スクリーンをクリアします。
748 suspended_screen_->internal_->TerminalSend(
749 suspended_screen_->internal_->ResetCursorPosition());
750 suspended_screen_->ResetPosition(
751 suspended_screen_->internal_->output_buffer,
752 /*clear=*/true);
753 suspended_screen_->dimx_ = 0;
754 suspended_screen_->dimy_ = 0;
755
756 // 次回スクリーンを再描画させるため、寸法をリセットします:
757 suspended_screen_->internal_->Uninstall();
758 }
759
760 // このスクリーンが現在アクティブです:
761 g_active_screen = public_;
762 g_active_screen->internal_->Install();
763
764 previous_animation_time_ = animation::Clock::now();
765}
766
767void App::Internal::PostMain() {
768 // カーソル位置を描画の末尾に配置します。
769 TerminalSend(ResetCursorPosition());
770
771 g_active_screen = nullptr;
772
773 // 一時停止していたスクリーンを復元します。
774 if (suspended_screen_) {
775 // スクリーンをクリアし、カーソルを描画の先頭に配置します。
776 public_->ResetPosition(output_buffer, /*clear=*/true);
777 public_->dimx_ = 0;
778 public_->dimy_ = 0;
779 Uninstall();
780 std::swap(g_active_screen, suspended_screen_);
781 g_active_screen->internal_->Install();
782 } else {
783 Uninstall();
784
785 std::cout << "\r";
786 // 最終終了時には、現在の描画を保持し、カーソル位置をその1行後に
787 // リセットします。
788 if (!use_alternative_screen_) {
789 std::cout << "\n";
790 }
791 std::cout << std::flush;
792 }
793
794 int sig = g_last_signal.exchange(0);
795 if (sig != 0) {
796 RestoreSignalHandlerAndRaise(sig);
797 }
798}
799
800bool App::Internal::HasQuitted() {
801 return quit_;
802}
803
804void App::Internal::RunOnce(const Component& component) {
805 const AutoReset set_component(&component_, component);
806 ExecuteSignalHandlers();
807 FetchTerminalEvents();
808
809 while (!quit_ && main_loop_receiver->Has()) {
810 public_->Post(main_loop_receiver->Pop());
811 }
812
813 // キューから保留中のタスクを実行します。
814 const size_t executed_task = task_runner.ExecutedTasks();
815 task_runner.RunUntilIdle();
816 // タスクが実行されなかった場合、スクリーンを再描画せずに早期リターンできます。
817 if (executed_task == task_runner.ExecutedTasks()) {
818 return;
819 }
820
821 ExecuteSignalHandlers();
822 Draw(component);
823
824 if (selection_data_previous_ != selection_data_) {
825 selection_data_previous_ = selection_data_;
826 if (selection_on_change_) {
827 selection_on_change_();
828 public_->Post(Event::Custom);
829 }
830 }
831}
832
833void App::Internal::RunOnceBlocking(Component component) {
834 // 最大で60FPSに設定します。
835 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
836
837 auto time = std::chrono::steady_clock::now();
838 const size_t executed_task = task_runner.ExecutedTasks();
839
840 // 少なくとも1つのタスクが実行されるまで待ちます。
841 while (executed_task == task_runner.ExecutedTasks() && !HasQuitted()) {
842 RunOnce(component);
843
844 const auto now = std::chrono::steady_clock::now();
845 const auto delta = now - time;
846 time = now;
847
848 if (delta < time_per_frame) {
849 const auto sleep_duration = time_per_frame - delta;
850 std::this_thread::sleep_for(sleep_duration);
851 }
852 }
853}
854
855void App::Internal::HandleTask(Component component, Task& task) {
856 std::visit(
857 [&](auto&& arg) {
858 using T = std::decay_t<decltype(arg)>;
859 // clang-format off
860
861 // イベントを処理します。
862 if constexpr (std::is_same_v<T, Event>) {
863
864 if (arg.is_cursor_position()) {
865 cursor_x_ = arg.cursor_x();
866 cursor_y_ = arg.cursor_y();
867 cursor_position_request.OnReply();
868 return;
869 }
870
871 if (arg.is_cursor_shape()) {
872 cursor_reset_shape_ = arg.cursor_shape();
873 return;
874 }
875
876 if (arg.IsTerminalCapabilities()) {
877 terminal_capabilities_ = arg.TerminalCapabilities();
878 return;
879 }
880
881 if (arg.IsTerminalNameVersion()) {
882 terminal_name_ = arg.TerminalName();
883 terminal_version_ = arg.TerminalVersion();
884 return;
885 }
886
887 if (arg.IsTerminalEmulator()) {
888 terminal_emulator_name_ = arg.TerminalEmulatorName();
889 terminal_emulator_version_ = arg.TerminalEmulatorVersion();
890 return;
891 }
892
893 if (arg.is_mouse()) {
894 arg.mouse().x -= cursor_x_;
895 arg.mouse().y -= cursor_y_;
896 }
897
898 arg.screen_ = public_;
899
900 bool handled = component->OnEvent(arg);
901 handled = HandleSelection(handled, arg);
902
903 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
904 RecordSignal(SIGINT);
905 }
906
907#if !defined(_WIN32)
908 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
909 RecordSignal(SIGTSTP);
910 }
911#endif
912
913 frame_valid_ = false;
914 return;
915 }
916
917 // コールバックを処理します
918 if constexpr (std::is_same_v<T, Closure>) {
919 arg();
920 return;
921 }
922
923 // アニメーションを処理します
924 if constexpr (std::is_same_v<T, AnimationTask>) {
925 if (!animation_requested_) {
926 return;
927 }
928
929 animation_requested_ = false;
930 const animation::TimePoint now = animation::Clock::now();
931 const animation::Duration delta = now - previous_animation_time_;
932 previous_animation_time_ = now;
933
934 animation::Params params(delta);
935 component->OnAnimation(params);
936 frame_valid_ = false;
937 return;
938 }
939 },
940 task);
941 // clang-format on
942}
943
944bool App::Internal::HandleSelection(bool handled, Event event) {
945 if (handled) {
946 selection_pending_ = nullptr;
947 selection_data_.empty = true;
948 selection_ = nullptr;
949 return true;
950 }
951
952 if (!event.is_mouse()) {
953 return false;
954 }
955
956 auto& mouse = event.mouse();
957 if (mouse.button != Mouse::Left) {
958 return false;
959 }
960
961 if (mouse.motion == Mouse::Pressed) {
962 selection_pending_ = public_->CaptureMouse();
963 selection_data_.start_x = mouse.x;
964 selection_data_.start_y = mouse.y;
965 selection_data_.end_x = mouse.x;
966 selection_data_.end_y = mouse.y;
967 return false;
968 }
969
970 if (!selection_pending_) {
971 return false;
972 }
973
974 if (mouse.motion == Mouse::Moved) {
975 if ((mouse.x != selection_data_.end_x) ||
976 (mouse.y != selection_data_.end_y)) {
977 selection_data_.end_x = mouse.x;
978 selection_data_.end_y = mouse.y;
979 selection_data_.empty = false;
980 }
981
982 return true;
983 }
984
985 if (mouse.motion == Mouse::Released) {
986 selection_pending_ = nullptr;
987 selection_data_.end_x = mouse.x;
988 selection_data_.end_y = mouse.y;
989 selection_data_.empty = false;
990 return true;
991 }
992
993 return false;
994}
995
996void App::Internal::Draw(Component component) {
997 if (frame_valid_) {
998 return;
999 }
1000 auto document = component->Render();
1001 int dimx = 0;
1002 int dimy = 0;
1003 auto terminal = Terminal::Size();
1004 document->ComputeRequirement();
1005 switch (dimension_) {
1007 dimx = public_->dimx_;
1008 dimy = public_->dimy_;
1009 break;
1011 dimx = terminal.dimx;
1012 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1013 break;
1015 dimx = terminal.dimx;
1016 dimy = terminal.dimy;
1017 break;
1019 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
1020 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1021 break;
1022 }
1023
1024 // リセット時のちらつきを防ぐためにカーソルを非表示にします。
1025 TerminalSend("\033[?25l");
1026
1027 const bool resized =
1028 frame_count_ == 0 || (dimx != public_->dimx_) || (dimy != public_->dimy_);
1029 TerminalSend(ResetCursorPosition());
1030
1031 if (frame_count_ != 0) {
1032 // 新しいフレームの描画を開始するため、カーソル位置を左下隅に
1033 // リセットします。
1034 public_->ResetPosition(output_buffer, resized);
1035
1036 // 端末の幅が減少すると、端末エミュレータは行の折り返しを開始し、
1037 // 表示を汚してしまいます。これは完全にクリアする必要があります。
1038 if ((dimx < public_->dimx_) && !use_alternative_screen_) {
1039 TerminalSend("\033[J"); // 端末出力をクリアする
1040 TerminalSend("\033[H"); // カーソルをホーム位置に移動する
1041 }
1042 }
1043
1044 // 必要に応じてスクリーンをリサイズします。
1045 if (resized) {
1046 public_->dimx_ = dimx;
1047 public_->dimy_ = dimy;
1048 public_->cells_ = std::vector<Cell>(static_cast<size_t>(dimx) *
1049 static_cast<size_t>(dimy));
1050 Cursor cursor = public_->cursor_;
1051 cursor.x = dimx - 1;
1052 cursor.y = dimy - 1;
1053 public_->SetCursor(cursor);
1054 }
1055
1056 // 定期的に、端末エミュレータにスクリーンに対するフレームの位置を
1057 // 要求します。これは、スクリーン座標で報告されたマウス位置を
1058 // フレーム座標に変換するのに役立ちます。
1059 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1060 RequestCursorPosition(previous_frame_resized_);
1061 }
1062 previous_frame_resized_ = resized;
1063
1064 selection_ = selection_data_.empty
1065 ? std::make_unique<Selection>()
1066 : std::make_unique<Selection>(
1067 selection_data_.start_x, selection_data_.start_y, //
1068 selection_data_.end_x, selection_data_.end_y);
1069 Render(*public_, document.get(), *selection_);
1070
1071 // CJK文字を挿入するツールを使用しているユーザーのためにカーソル位置を設定します。
1072 {
1073 const int dx = public_->dimx_ - 1 - public_->cursor_.x +
1074 int(public_->dimx_ != terminal.dimx);
1075 const int dy = public_->dimy_ - 1 - public_->cursor_.y;
1076
1077 set_cursor_position_.clear();
1078 reset_cursor_position_.clear();
1079
1080 if (dy != 0) {
1081 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
1082 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
1083 }
1084
1085 if (dx != 0) {
1086 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
1087 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
1088 }
1089
1090 if (public_->cursor_.shape != Screen::Cursor::Hidden) {
1091 set_cursor_position_ += "\033[?25h";
1092 set_cursor_position_ +=
1093 "\033[" + std::to_string(int(public_->cursor_.shape)) + " q";
1094 }
1095 }
1096
1097 public_->ToString(output_buffer);
1098 TerminalSend(set_cursor_position_);
1099 TerminalFlush();
1100
1101 public_->Clear();
1102 frame_valid_ = true;
1103 frame_count_++;
1104}
1105
1106std::string App::Internal::ResetCursorPosition() {
1107 std::string result = std::move(reset_cursor_position_);
1108 reset_cursor_position_ = "";
1109 return result;
1110}
1111
1112void App::Internal::RequestCursorPosition(bool force) {
1113 cursor_position_request.Request(force);
1114}
1115
1116void App::Internal::TerminalSend(std::string_view s) {
1117 output_buffer += s;
1118}
1119
1120void App::Internal::TerminalFlush() {
1121 // Emscriptenはflushを実装していません。0をflushとして解釈します。
1122 output_buffer += '\0';
1123 std::cout << output_buffer << std::flush;
1124 output_buffer.clear();
1125}
1126
1127void App::Internal::InstallPipedInputHandling() {
1128 is_stdin_a_tty_ = false;
1129 is_stdout_a_tty_ = false;
1130#if defined(__EMSCRIPTEN__)
1131 is_stdin_a_tty_ = true;
1132 is_stdout_a_tty_ = true;
1133#elif defined(_WIN32)
1134 is_stdin_a_tty_ = _isatty(_fileno(stdin));
1135 is_stdout_a_tty_ = _isatty(_fileno(stdout));
1136#else
1137 tty_fd_ = STDIN_FILENO;
1138 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
1139 // アプリケーションによって明示的に有効にされている場合、パイプ入力リダイレクトを処理します。
1140 // これにより、アプリケーションはstdinからデータを読み取りながら、対話的に使用するためにターミナルからキーボード入力を引き続き受信できます。
1141 if (!handle_piped_input_) {
1142 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1143 } else if (isatty(STDIN_FILENO)) {
1144 is_stdin_a_tty_ = true;
1145 } else {
1146 // キーボード入力のために`/dev/tty`を開きます。
1147 tty_fd_ = open("/dev/tty", O_RDONLY); // NOLINT
1148 if (tty_fd_ < 0) {
1149 // `/dev/tty`のオープンに失敗しました(コンテナ、ヘッドレスシステムなど)
1150 // stdinにフォールバックします。 return;
1151 tty_fd_ = STDIN_FILENO; // stdinにフォールバックします。
1152 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1153 } else {
1154 is_stdin_a_tty_ = true;
1155 // 終了時に`/dev/tty`ファイルディスクリプタを閉じます。
1156 on_exit_functions.emplace([this] {
1157 close(tty_fd_);
1158 tty_fd_ = -1;
1159 });
1160 }
1161 }
1162#endif
1163}
1164
1165void App::Internal::InstallTerminalInfo() {
1166 // 端末に現在のカーソル形状を報告するよう要求します。終了時に
1167 // 復元します。
1168 if (is_stdout_a_tty_) {
1169 TerminalSend(DECRQSS_DECSCUSR);
1170 TerminalSend("\033[>q"); // XTVERSION
1171 TerminalSend("\033[>c"); // DA2
1172 TerminalSend("\033[c"); // DA1
1173 TerminalFlush();
1174 }
1175
1176 // setupヘッドを使用してカーソル形状の返信を待ちます。
1177 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
1178 // setupにスコープされたレシーバー: setup後もこれを生かしておくと、
1179 // 以降のすべてのイベントがバッファ内で固定され、アプリの寿命全体に
1180 // わたってバッファが増大してしまいます。
1181 auto setup_receiver = event_buffer.CreateReceiver();
1182 auto start = std::chrono::steady_clock::now();
1183 bool terminal_capabilities_received = false;
1184 // setupヘッドを使用してカーソル形状の返信を待ちます。
1185 while (true) {
1186 FetchTerminalEvents();
1187 while (setup_receiver->Has()) {
1188 const auto event = setup_receiver->Pop();
1189 if (event.is_cursor_shape()) {
1190 cursor_reset_shape_ = event.cursor_shape();
1191 }
1192
1193 if (event.IsTerminalCapabilities()) {
1194 terminal_capabilities_ = event.TerminalCapabilities();
1195 terminal_capabilities_received = true;
1196 }
1197
1198 if (event.IsTerminalNameVersion()) {
1199 terminal_name_ = event.TerminalName();
1200 terminal_version_ = event.TerminalVersion();
1201 }
1202
1203 if (event.IsTerminalEmulator()) {
1204 terminal_emulator_name_ = event.TerminalEmulatorName();
1205 terminal_emulator_version_ = event.TerminalEmulatorVersion();
1206 }
1207 }
1208
1209 // 応答は順番に受信されることが期待されているため、最後のもの
1210 // (XTVERSION) を受信したらbreakできます。また、端末がこれらの
1211 // クエリをサポートしない場合に永遠に待たないよう、タイムアウトも設定します。
1212 if (terminal_capabilities_received) {
1213 break;
1214 }
1215
1216 if (std::chrono::steady_clock::now() - start >
1217 std::chrono::milliseconds(500)) {
1218 break;
1219 }
1220 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1221 }
1222 }
1223
1224 // 端末識別情報に基づいてクイークとカラーサポートを設定します。
1225 Terminal::Quirks quirks = Terminal::GetQuirks();
1226
1227 auto color_support = Terminal::ComputeColorSupport(
1228 util::GetEnv("TERM"), util::GetEnv("COLORTERM"),
1229 util::GetEnv("TERM_PROGRAM"), terminal_name_, terminal_emulator_name_,
1230 terminal_capabilities_);
1231
1232 quirks.SetColorSupport(color_support);
1233
1234 const bool is_modern_emulator = (terminal_emulator_name_ != "unknown");
1235 const bool is_vt220_plus =
1236 (terminal_name_ != "vt100" && terminal_name_ != "unknown");
1237 bool reports_utf8 = false;
1238 for (const int x : terminal_capabilities_) {
1239 if (x == 52) {
1240 reports_utf8 = true;
1241 break;
1242 }
1243 }
1244
1245 // ヒューリスティック: 端末エミュレータがモダンである、またはUTF-8や
1246 // カラーのサポートを報告している場合、ブロック文字やカーソル非表示を
1247 // サポートしていると想定できます。これらは良い体験のために不可欠です。
1248 // これはヒューリスティックですが、これらの機能をサポートしない古い
1249 // 端末エミュレータを回避しつつ、これらの機能をサポートするモダンな
1250 // 端末エミュレータでは良い体験を提供することができます。
1251 bool modern = is_modern_emulator || is_vt220_plus || reports_utf8;
1252 if (modern) {
1253 quirks.SetBlockCharacters(true);
1254 quirks.SetCursorHiding(true);
1255 quirks.SetComponentAscii(false);
1256 }
1257
1258 Terminal::SetQuirks(quirks);
1259
1260 on_exit_functions.emplace([this] {
1261 TerminalSend("\033[?25h"); // カーソルを有効にします。
1262 if (is_stdout_a_tty_) {
1263 TerminalSend("\033[" + std::to_string(cursor_reset_shape_) + " q");
1264 }
1265 });
1266}
1267
1268void App::Internal::Signal(int signal) {
1269 if (signal == SIGABRT) {
1270 public_->Exit();
1271 return;
1272 }
1273
1274// Windows は SIGTSTP / SIGWINCH をサポートしていません
1275#if !defined(_WIN32)
1276 if (signal == SIGTSTP) {
1277 public_->Post([&] {
1278 TerminalSend(ResetCursorPosition());
1279 public_->ResetPosition(output_buffer, /*clear*/ true);
1280 Uninstall();
1281 public_->dimx_ = 0;
1282 public_->dimy_ = 0;
1283 (void)std::raise(SIGTSTP);
1284 Install();
1285 });
1286 return;
1287 }
1288
1289 if (signal == SIGWINCH) {
1290 public_->Post(Event::Special({0}));
1291 return;
1292 }
1293#endif
1294}
1295
1296size_t App::Internal::FetchTerminalEvents() {
1297#if defined(_WIN32)
1298 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1299 // コンソールに入力があるかを確認します。
1300 auto console = GetStdHandle(STD_INPUT_HANDLE);
1301 DWORD number_of_events = 0;
1302 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1303 return std::vector<INPUT_RECORD>();
1304 }
1305 if (number_of_events <= 0) {
1306 // 入力がない場合、戻ります。
1307 return std::vector<INPUT_RECORD>();
1308 }
1309 // 入力イベントを読み取ります。
1310 std::vector<INPUT_RECORD> records(number_of_events);
1311 DWORD number_of_events_read = 0;
1312 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1313 &number_of_events_read)) {
1314 return std::vector<INPUT_RECORD>();
1315 }
1316 records.resize(number_of_events_read);
1317 return records;
1318 };
1319
1320 auto records = get_input_records();
1321 if (records.size() == 0) {
1322 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1323 const size_t timeout_microseconds =
1324 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1325 terminal_input_parser.Timeout(timeout_microseconds);
1326 return 0;
1327 }
1328 last_char_time = std::chrono::steady_clock::now();
1329
1330 // 入力イベントをFTXUIイベントに変換します。
1331 // 各イベントに対して、ターミナル入力パーサーを呼び出してEventに変換します。
1332 std::wstring wstring;
1333 for (const auto& r : records) {
1334 switch (r.EventType) {
1335 case KEY_EVENT: {
1336 auto key_event = r.Event.KeyEvent;
1337 // UPキーイベントを無視する
1338 if (key_event.bKeyDown == FALSE) {
1339 continue;
1340 }
1341 const wchar_t wc = key_event.uChar.UnicodeChar;
1342 wstring += wc;
1343 if (wc >= 0xd800 && wc <= 0xdbff) {
1344 // 次のレコードでLow Surrogateが到着するのを待ちます。
1345 continue;
1346 }
1347 for (auto it : to_string(wstring)) {
1348 terminal_input_parser.Add(it);
1349 }
1350 wstring.clear();
1351 } break;
1352 case WINDOW_BUFFER_SIZE_EVENT:
1353 public_->Post(Event::Special({0}));
1354 break;
1355 case MENU_EVENT:
1356 case FOCUS_EVENT:
1357 case MOUSE_EVENT:
1358 // TODO(mauve): 後で実装します。
1359 break;
1360 }
1361 }
1362 return records.size();
1363#elif defined(__EMSCRIPTEN__)
1364 // 端末から文字を読み取ります。
1365 // ノンブロッキングになるよう設定しています。
1366 std::array<char, 4096> out{};
1367 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1368 if (l <= 0) {
1369 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1370 const size_t timeout_microseconds =
1371 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1372 terminal_input_parser.Timeout(timeout_microseconds);
1373 return 0;
1374 }
1375 last_char_time = std::chrono::steady_clock::now();
1376
1377 // 文字をイベントに変換します。
1378 for (ssize_t i = 0; i < l; ++i) {
1379 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1380 }
1381 return (size_t)l;
1382#else // POSIX (Linux & Mac)
1383 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1384 const int poll_result = poll(&pfd, 1, 0);
1385 if (poll_result <= 0) {
1386 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1387 const size_t timeout_ms =
1388 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1389 terminal_input_parser.Timeout(static_cast<int>(timeout_ms));
1390 return 0;
1391 }
1392 last_char_time = std::chrono::steady_clock::now();
1393
1394 // 利用可能な入力をすべて排出し、バースト(例: 高速なマウスホイール
1395 // スクロール)がフレームをまたいで蓄積しないようにします。合計は、
1396 // 連続的な入力の氾濫下でもフレームの応答性を保つために制限されて
1397 // います。#1348を参照してください。
1398 constexpr size_t kMaxBytesPerFetch = 64 * 1024;
1399 std::array<char, 4096> out{};
1400 size_t total = 0;
1401 while (total < kMaxBytesPerFetch) {
1402 const ssize_t l = read(tty_fd_, out.data(), out.size());
1403 if (l <= 0) {
1404 break;
1405 }
1406
1407 // 文字をイベントに変換します。
1408 for (ssize_t i = 0; i < l; ++i) {
1409 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1410 }
1411 total += static_cast<size_t>(l);
1412
1413 pfd.revents = 0;
1414 if (poll(&pfd, 1, 0) <= 0) {
1415 break;
1416 }
1417 }
1418 return total;
1419#endif
1420}
1421
1422void App::Internal::PostAnimationTask() {
1423 public_->Post(AnimationTask());
1424
1425 // アニメーションタスクを15msごとに繰り返します。これは約66fpsのフレームレートに相当します。
1426 task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1427 std::chrono::milliseconds(15));
1428}
1429
1430App::App(std::unique_ptr<Internal> internal, int dimx, int dimy)
1431 : Screen(dimx, dimy), internal_(std::move(internal)) {
1432 internal_->public_ = this;
1433}
1434
1435App::App(App&& other) noexcept : Screen(std::move(other)) {
1436 internal_ = std::move(other.internal_);
1437 if (internal_) {
1438 internal_->public_ = this;
1439 }
1440}
1441
1442App& App::operator=(App&& other) noexcept {
1443 Screen::operator=(std::move(other));
1444 internal_ = std::move(other.internal_);
1445 if (internal_) {
1446 internal_->public_ = this;
1447 }
1448 return *this;
1449}
1450
1451App::~App() = default;
1452
1453// static
1454App App::FixedSize(int dimx, int dimy) {
1455 auto internal =
1456 std::make_unique<Internal>(nullptr, AppDimension::Fixed, false);
1457 return App(std::move(internal), dimx, dimy);
1458}
1459
1460// static
1461App App::Fullscreen() {
1462 return FullscreenAlternateScreen();
1463}
1464
1465// static
1466App App::FullscreenPrimaryScreen() {
1467 auto terminal = Terminal::Size();
1468 auto internal =
1469 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, false);
1470 return App(std::move(internal), terminal.dimx, terminal.dimy);
1471}
1472
1473// static
1474App App::FullscreenAlternateScreen() {
1475 auto terminal = Terminal::Size();
1476 auto internal =
1477 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, true);
1478 return App(std::move(internal), terminal.dimx, terminal.dimy);
1479}
1480
1481// static
1482App App::FitComponent() {
1483 auto terminal = Terminal::Size();
1484 auto internal =
1485 std::make_unique<Internal>(nullptr, AppDimension::FitComponent, false);
1486 return App(std::move(internal), terminal.dimx, terminal.dimy);
1487}
1488
1489// static
1490App App::TerminalOutput() {
1491 auto terminal = Terminal::Size();
1492 auto internal =
1493 std::make_unique<Internal>(nullptr, AppDimension::TerminalOutput, false);
1494 return App(std::move(internal), terminal.dimx, terminal.dimy);
1495}
1496
1497void App::TrackMouse(bool enable) {
1498 internal_->track_mouse_ = enable;
1499}
1500
1501void App::HandlePipedInput(bool enable) {
1502 internal_->handle_piped_input_ = enable;
1503}
1504
1505// static
1506App* App::Active() {
1507 return g_active_screen;
1508}
1509
1510void App::Loop(Component component) {
1511 class Loop loop(this, std::move(component));
1512 loop.Run();
1513}
1514
1515void App::Exit() {
1516 Post([this] { internal_->ExitNow(); });
1517}
1518
1519Closure App::ExitLoopClosure() {
1520 return [this] { Exit(); };
1521}
1522
1523void App::Post(Task task) {
1524 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
1525 if (internal_->component_) {
1526 internal_->HandleTask(internal_->component_, task);
1527 return;
1528 }
1529
1530 // コンポーネントがない場合でも、クロージャは実行できます。
1531 if (std::holds_alternative<Closure>(task)) {
1532 std::get<Closure>(task)();
1533 }
1534 });
1535}
1536
1537void App::PostEvent(Event event) {
1538 // PostEventはスレッドセーフであるとドキュメント化されています:
1539 // ミューテックスで保護されたタスクキューを経由します。event_bufferは
1540 // メインスレッドからのみ安全に使用できます。
1541 Post(Task(std::move(event)));
1542}
1543
1544// static
1545void App::PostEventOrExecute(Closure closure) {
1546 if (!closure) {
1547 return;
1548 }
1549 if (auto* app = App::Active()) {
1550 app->Post(std::move(closure));
1551 } else {
1552 closure();
1553 }
1554}
1555
1556void App::RequestAnimationFrame() {
1557 if (internal_->animation_requested_) {
1558 return;
1559 }
1560 internal_->animation_requested_ = true;
1561 auto now = animation::Clock::now();
1562 const auto time_histeresis = std::chrono::milliseconds(33);
1563 if (now - internal_->previous_animation_time_ >= time_histeresis) {
1564 internal_->previous_animation_time_ = now;
1565 }
1566}
1567
1568CapturedMouse App::CaptureMouse() {
1569 if (internal_->mouse_captured) {
1570 return nullptr;
1571 }
1572 internal_->mouse_captured = true;
1573 return std::make_unique<CapturedMouseImpl>(
1574 [this] { internal_->mouse_captured = false; });
1575}
1576
1577Closure App::WithRestoredIO(Closure fn) {
1578 return [this, fn] {
1579 internal_->Uninstall();
1580 fn();
1581 internal_->Install();
1582 };
1583}
1584
1585void App::ForceHandleCtrlC(bool force) {
1586 internal_->force_handle_ctrl_c_ = force;
1587}
1588
1589void App::ForceHandleCtrlZ(bool force) {
1590 internal_->force_handle_ctrl_z_ = force;
1591}
1592
1593std::string App::GetSelection() {
1594 if (!internal_->selection_) {
1595 return "";
1596 }
1597 return internal_->selection_->GetParts();
1598}
1599
1600void App::SelectionChange(std::function<void()> callback) {
1601 internal_->selection_on_change_ = std::move(callback);
1602}
1603
1604const std::string& App::TerminalName() const {
1605 return internal_->terminal_name_;
1606}
1607
1608int App::TerminalVersion() const {
1609 return internal_->terminal_version_;
1610}
1611
1612const std::string& App::TerminalEmulatorName() const {
1613 return internal_->terminal_emulator_name_;
1614}
1615
1616const std::string& App::TerminalEmulatorVersion() const {
1617 return internal_->terminal_emulator_version_;
1618}
1619
1620const std::vector<int>& App::TerminalCapabilities() const {
1621 return internal_->terminal_capabilities_;
1622}
1623
1624std::vector<std::string> App::TerminalCapabilityNames() const {
1625 return Event::TerminalCapabilities("", internal_->terminal_capabilities_)
1626 .TerminalCapabilityNames();
1627}
1628
1629// ループはこれらを呼び出します:
1630
1631void App::ExitNow() {
1632 internal_->ExitNow();
1633}
1634void App::Install() {
1635 internal_->Install();
1636}
1637void App::Uninstall() {
1638 internal_->Uninstall();
1639}
1640void App::PreMain() {
1641 internal_->PreMain();
1642}
1643void App::PostMain() {
1644 internal_->PostMain();
1645}
1646bool App::HasQuitted() {
1647 return internal_->HasQuitted();
1648}
1649void App::RunOnce(const Component& component) {
1650 internal_->RunOnce(component);
1651}
1652void App::RunOnceBlocking(Component component) {
1653 internal_->RunOnceBlocking(component);
1654}
1655void App::HandleTask(Component component, Task& task) {
1656 internal_->HandleTask(component, task);
1657}
1658bool App::HandleSelection(bool handled, Event event) {
1659 return internal_->HandleSelection(handled, event);
1660}
1661void App::Draw(Component component) {
1662 internal_->Draw(component);
1663}
1664std::string App::ResetCursorPosition() {
1665 return internal_->ResetCursorPosition();
1666}
1667void App::RequestCursorPosition(bool force) {
1668 internal_->RequestCursorPosition(force);
1669}
1670void App::TerminalSend(std::string_view s) {
1671 internal_->TerminalSend(s);
1672}
1673void App::TerminalFlush() {
1674 internal_->TerminalFlush();
1675}
1676void App::InstallPipedInputHandling() {
1677 internal_->InstallPipedInputHandling();
1678}
1679void App::InstallTerminalInfo() {
1680 internal_->InstallTerminalInfo();
1681}
1682void App::Signal(int signal) {
1683 internal_->Signal(signal);
1684}
1685size_t App::FetchTerminalEvents() {
1686 return internal_->FetchTerminalEvents();
1687}
1688void App::PostAnimationTask() {
1689 internal_->PostAnimationTask();
1690}
1691
1692Loop::Loop(App* screen, Component component)
1693 : screen_(screen), component_(std::move(component)) {
1694 screen_->PreMain();
1695}
1696
1697Loop::~Loop() {
1698 screen_->PostMain();
1699}
1700
1701bool Loop::HasQuitted() {
1702 return screen_->HasQuitted();
1703}
1704
1705void Loop::RunOnce() {
1706 screen_->RunOnce(component_);
1707}
1708
1709void Loop::RunOnceBlocking() {
1710 screen_->RunOnceBlocking(component_);
1711}
1712
1713void Loop::Run() {
1714 while (!HasQuitted()) {
1715 RunOnceBlocking();
1716 }
1717}
1718
1719} // namespace ftxui
Quirks GetQuirks()
ターミナルのクイークを取得します。
Definition terminal.cpp:350
Dimensions Size()
ターミナルサイズを取得します。
Definition terminal.cpp:299
void SetQuirks(const Quirks &quirks)
ターミナルのクイークを上書きします。
Definition terminal.cpp:360
FTXUI ftxui::animation::名前空間
void SetFallbackSize(const Dimensions &fallbackSize)
自動検出が失敗した場合にターミナルサイズを上書きします
Definition terminal.cpp:327
Color ComputeColorSupport(std::string_view term, std::string_view colorterm, std::string_view term_program, std::string_view terminal_name, std::string_view terminal_emulator_name, const std::vector< int > &capabilities)
環境変数とターミナル識別に基づいて色サポートを計算します。
Definition terminal.cpp:192
std::chrono::duration< float > Duration
Definition animation.hpp:26
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:25
void RequestAnimationFrame()
Definition app.cpp:74
const char * GetEnv(const char *name)
Definition util.hpp:18
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:13
FTXUI ftxui::名前空間
Definition animation.hpp:11
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(std::wstring_view s)
std::wstringをUTF8 std::stringに変換します。
Definition string.cpp:547
AppDimension
Definition app.cpp:66
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
void Render(Screen &screen, Node *node, Selection &selection)
Definition node.cpp:105
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component
Definition app.hpp:23