aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/consoletui.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenutil/consoletui.cpp')
-rw-r--r--src/zenutil/consoletui.cpp483
1 files changed, 483 insertions, 0 deletions
diff --git a/src/zenutil/consoletui.cpp b/src/zenutil/consoletui.cpp
new file mode 100644
index 000000000..4410d463d
--- /dev/null
+++ b/src/zenutil/consoletui.cpp
@@ -0,0 +1,483 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/consoletui.h>
+
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+#else
+# include <poll.h>
+# include <sys/ioctl.h>
+# include <termios.h>
+# include <unistd.h>
+#endif
+
+#include <cstdio>
+
+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<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(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<int>(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<uint32_t>(Csbi.dwSize.X);
+ }
+#else
+ struct winsize Ws = {};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &Ws) == 0 && Ws.ws_col > 0)
+ {
+ return static_cast<uint32_t>(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<const std::string> Items)
+{
+ EnableVirtualTerminal();
+
+#if ZEN_PLATFORM_WINDOWS
+ ConsoleCodePageGuard CodePageGuard(CP_UTF8);
+#else
+ RawModeGuard RawMode;
+ if (!RawMode.IsValid())
+ {
+ return -1;
+ }
+#endif
+
+ const int Count = static_cast<int>(Items.size());
+ int SelectedIndex = 0;
+
+ printf("\n%.*s\n\n", static_cast<int>(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<tcflag_t>(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ Raw.c_cflag |= CS8;
+ Raw.c_lflag &= ~static_cast<tcflag_t>(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<uint32_t>(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<uint32_t>(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
+}
+
+} // namespace zen