// Copyright Epic Games, Inc. All Rights Reserved. #include #include #if ZEN_PLATFORM_WINDOWS # include #else # include # include # include # include #endif #include namespace zen { ////////////////////////////////////////////////////////////////////////// // Platform-specific terminal helpers #if ZEN_PLATFORM_WINDOWS static bool CheckIsInteractiveTerminal() { DWORD dwMode = 0; return GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &dwMode) && GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode); } static void EnableVirtualTerminal() { HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); DWORD dwMode = 0; if (GetConsoleMode(hStdOut, &dwMode)) { SetConsoleMode(hStdOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } // RAII guard: sets the console output code page for the lifetime of the object and // restores the original on destruction. Required for UTF-8 glyphs to render correctly // via printf/fflush since the default console code page is not UTF-8. class ConsoleCodePageGuard { public: explicit ConsoleCodePageGuard(UINT NewCP) : m_OldCP(GetConsoleOutputCP()) { SetConsoleOutputCP(NewCP); } ~ConsoleCodePageGuard() { SetConsoleOutputCP(m_OldCP); } private: UINT m_OldCP; }; enum class ConsoleKey { Unknown, ArrowUp, ArrowDown, Enter, Escape, }; static ConsoleKey ReadKey() { HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); INPUT_RECORD Record{}; DWORD dwRead = 0; while (true) { if (!ReadConsoleInputA(hStdin, &Record, 1, &dwRead)) { return ConsoleKey::Escape; // treat read error as cancel } if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown) { switch (Record.Event.KeyEvent.wVirtualKeyCode) { case VK_UP: return ConsoleKey::ArrowUp; case VK_DOWN: return ConsoleKey::ArrowDown; case VK_RETURN: return ConsoleKey::Enter; case VK_ESCAPE: return ConsoleKey::Escape; default: break; } } } } #else // POSIX static bool CheckIsInteractiveTerminal() { return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO); } static void EnableVirtualTerminal() { // ANSI escape codes are native on POSIX terminals; nothing to do } // RAII guard: switches the terminal to raw/unbuffered input mode and restores // the original attributes on destruction. class RawModeGuard { public: RawModeGuard() { if (tcgetattr(STDIN_FILENO, &m_OldAttrs) != 0) { return; } struct termios Raw = m_OldAttrs; Raw.c_iflag &= ~static_cast(BRKINT | ICRNL | INPCK | ISTRIP | IXON); Raw.c_cflag |= CS8; Raw.c_lflag &= ~static_cast(ECHO | ICANON | IEXTEN | ISIG); Raw.c_cc[VMIN] = 1; Raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0) { m_Valid = true; } } ~RawModeGuard() { if (m_Valid) { tcsetattr(STDIN_FILENO, TCSANOW, &m_OldAttrs); } } bool IsValid() const { return m_Valid; } private: struct termios m_OldAttrs = {}; bool m_Valid = false; }; static int ReadByteWithTimeout(int TimeoutMs) { struct pollfd Pfd { STDIN_FILENO, POLLIN, 0 }; if (poll(&Pfd, 1, TimeoutMs) > 0 && (Pfd.revents & POLLIN)) { unsigned char c = 0; if (read(STDIN_FILENO, &c, 1) == 1) { return static_cast(c); } } return -1; } // State for fullscreen live mode (alternate screen + raw input) static struct termios s_SavedAttrs = {}; static bool s_InLiveMode = false; enum class ConsoleKey { Unknown, ArrowUp, ArrowDown, Enter, Escape, }; static ConsoleKey ReadKey() { unsigned char c = 0; if (read(STDIN_FILENO, &c, 1) != 1) { return ConsoleKey::Escape; // treat read error as cancel } if (c == 27) // ESC byte or start of an escape sequence { int Next = ReadByteWithTimeout(50); if (Next == '[') { int Final = ReadByteWithTimeout(50); if (Final == 'A') { return ConsoleKey::ArrowUp; } if (Final == 'B') { return ConsoleKey::ArrowDown; } } return ConsoleKey::Escape; } if (c == '\r' || c == '\n') { return ConsoleKey::Enter; } return ConsoleKey::Unknown; } #endif // ZEN_PLATFORM_WINDOWS / POSIX ////////////////////////////////////////////////////////////////////////// // Public API uint32_t TuiConsoleColumns(uint32_t Default) { #if ZEN_PLATFORM_WINDOWS CONSOLE_SCREEN_BUFFER_INFO Csbi = {}; if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi)) { return static_cast(Csbi.dwSize.X); } #else struct winsize Ws = {}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_col > 0) { return static_cast(Ws.ws_col); } #endif return Default; } void TuiEnableOutput() { EnableVirtualTerminal(); #if ZEN_PLATFORM_WINDOWS SetConsoleOutputCP(CP_UTF8); #endif } bool TuiIsStdoutTty() { #if ZEN_PLATFORM_WINDOWS static bool Cached = [] { DWORD dwMode = 0; return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwMode) != 0; }(); return Cached; #else static bool Cached = isatty(STDOUT_FILENO) != 0; return Cached; #endif } bool IsTuiAvailable() { static bool Cached = CheckIsInteractiveTerminal(); return Cached; } int TuiPickOne(std::string_view Title, std::span Items) { EnableVirtualTerminal(); #if ZEN_PLATFORM_WINDOWS ConsoleCodePageGuard CodePageGuard(CP_UTF8); #else RawModeGuard RawMode; if (!RawMode.IsValid()) { return -1; } #endif const int Count = static_cast(Items.size()); int SelectedIndex = 0; printf("\n%.*s\n\n", static_cast(Title.size()), Title.data()); // Hide cursor during interaction printf("\033[?25l"); // Renders the full entry list and hint footer. // On subsequent calls, moves the cursor back up first to overwrite the previous output. bool FirstRender = true; auto RenderAll = [&] { if (!FirstRender) { printf("\033[%dA", Count + 2); // move up: entries + blank line + hint line } FirstRender = false; for (int i = 0; i < Count; ++i) { bool IsSelected = (i == SelectedIndex); printf("\r\033[K"); // erase line if (IsSelected) { printf("\033[1;7m"); // bold + reverse video } // \xe2\x96\xb6 = U+25B6 BLACK RIGHT-POINTING TRIANGLE (▶) const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " "; printf("%s%s", Indicator, Items[i].c_str()); if (IsSelected) { printf("\033[0m"); // reset attributes } printf("\n"); } // Blank separator line printf("\r\033[K\n"); // Hint footer // \xe2\x86\x91 = U+2191 ↑ \xe2\x86\x93 = U+2193 ↓ printf( "\r\033[K \033[2m\xe2\x86\x91/\xe2\x86\x93\033[0m navigate " "\033[2mEnter\033[0m confirm " "\033[2mEsc\033[0m cancel\n"); fflush(stdout); }; RenderAll(); int Result = -1; bool Done = false; while (!Done) { ConsoleKey Key = ReadKey(); switch (Key) { case ConsoleKey::ArrowUp: SelectedIndex = (SelectedIndex - 1 + Count) % Count; RenderAll(); break; case ConsoleKey::ArrowDown: SelectedIndex = (SelectedIndex + 1) % Count; RenderAll(); break; case ConsoleKey::Enter: Result = SelectedIndex; Done = true; break; case ConsoleKey::Escape: Done = true; break; default: break; } } // Restore cursor and add a blank line for visual separation printf("\033[?25h\n"); fflush(stdout); return Result; } void TuiEnterAlternateScreen() { EnableVirtualTerminal(); #if ZEN_PLATFORM_WINDOWS SetConsoleOutputCP(CP_UTF8); #endif printf("\033[?1049h"); // Enter alternate screen buffer printf("\033[?25l"); // Hide cursor fflush(stdout); #if !ZEN_PLATFORM_WINDOWS if (tcgetattr(STDIN_FILENO, &s_SavedAttrs) == 0) { struct termios Raw = s_SavedAttrs; Raw.c_iflag &= ~static_cast(BRKINT | ICRNL | INPCK | ISTRIP | IXON); Raw.c_cflag |= CS8; Raw.c_lflag &= ~static_cast(ECHO | ICANON | IEXTEN | ISIG); Raw.c_cc[VMIN] = 1; Raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSANOW, &Raw) == 0) { s_InLiveMode = true; } } #endif } void TuiExitAlternateScreen() { printf("\033[?25h"); // Show cursor printf("\033[?1049l"); // Exit alternate screen buffer fflush(stdout); #if !ZEN_PLATFORM_WINDOWS if (s_InLiveMode) { tcsetattr(STDIN_FILENO, TCSANOW, &s_SavedAttrs); s_InLiveMode = false; } #endif } void TuiCursorHome() { printf("\033[H"); } uint32_t TuiConsoleRows(uint32_t Default) { #if ZEN_PLATFORM_WINDOWS CONSOLE_SCREEN_BUFFER_INFO Csbi = {}; if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Csbi)) { return static_cast(Csbi.srWindow.Bottom - Csbi.srWindow.Top + 1); } #else struct winsize Ws = {}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_row > 0) { return static_cast(Ws.ws_row); } #endif return Default; } bool TuiPollQuit() { #if ZEN_PLATFORM_WINDOWS HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); DWORD dwCount = 0; if (!GetNumberOfConsoleInputEvents(hStdin, &dwCount) || dwCount == 0) { return false; } INPUT_RECORD Record{}; DWORD dwRead = 0; while (PeekConsoleInputA(hStdin, &Record, 1, &dwRead) && dwRead > 0) { ReadConsoleInputA(hStdin, &Record, 1, &dwRead); if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown) { WORD vk = Record.Event.KeyEvent.wVirtualKeyCode; char ch = Record.Event.KeyEvent.uChar.AsciiChar; if (vk == VK_ESCAPE || ch == 'q' || ch == 'Q') { return true; } } } return false; #else // Non-blocking read: character 3 = Ctrl+C, 27 = Esc, 'q'/'Q' = quit int b = ReadByteWithTimeout(0); return (b == 3 || b == 27 || b == 'q' || b == 'Q'); #endif } TuiInput TuiPollInput() { #if ZEN_PLATFORM_WINDOWS HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); DWORD dwCount = 0; if (!GetNumberOfConsoleInputEvents(hStdin, &dwCount) || dwCount == 0) { return TuiInput::None; } INPUT_RECORD Record{}; DWORD dwRead = 0; while (PeekConsoleInputA(hStdin, &Record, 1, &dwRead) && dwRead > 0) { ReadConsoleInputA(hStdin, &Record, 1, &dwRead); if (Record.EventType == KEY_EVENT && Record.Event.KeyEvent.bKeyDown) { WORD vk = Record.Event.KeyEvent.wVirtualKeyCode; char ch = Record.Event.KeyEvent.uChar.AsciiChar; if (vk == VK_ESCAPE || ch == 'q' || ch == 'Q') { return TuiInput::Quit; } if (vk == VK_F5) { return TuiInput::Refresh; } if (vk == VK_UP) { return TuiInput::ArrowUp; } if (vk == VK_DOWN) { return TuiInput::ArrowDown; } } } return TuiInput::None; #else int b = ReadByteWithTimeout(0); if (b == 3 || b == 'q' || b == 'Q') { return TuiInput::Quit; } if (b == 27) { // Could be bare Esc or start of an escape sequence (e.g. \033[A) int Next = ReadByteWithTimeout(50); if (Next == '[') { int Final = ReadByteWithTimeout(50); if (Final == 'A') { return TuiInput::ArrowUp; } if (Final == 'B') { return TuiInput::ArrowDown; } // F5 = \033[15~ if (Final == '1') { int b2 = ReadByteWithTimeout(50); if (b2 == '5') { int b3 = ReadByteWithTimeout(50); if (b3 == '~') { return TuiInput::Refresh; } } } } return TuiInput::Quit; // bare Esc } return TuiInput::None; #endif } bool TuiWaitForInput(uint32_t TimeoutMs) { #if ZEN_PLATFORM_WINDOWS HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); return WaitForSingleObject(hStdin, TimeoutMs) == WAIT_OBJECT_0; #else struct pollfd Pfd { STDIN_FILENO, POLLIN, 0 }; return poll(&Pfd, 1, static_cast(TimeoutMs)) > 0; #endif } void TuiClearToBottom() { printf("\033[J"); } } // namespace zen