// Copyright Epic Games, Inc. All Rights Reserved. #include #include #if ZEN_PLATFORM_WINDOWS # include #else # include # include # include # include # include #endif #include #include #include #include #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; }; static char s_LastChar = 0; #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 } // SIGWINCH (terminal resize) flag — set by signal handler, consumed by TuiReadKey() static volatile sig_atomic_t s_GotSigWinch = 0; static void SigWinchHandler(int /*Sig*/) { s_GotSigWinch = 1; } // 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; } // Install SIGWINCH handler for terminal resize detection s_GotSigWinch = 0; struct sigaction Sa = {}; Sa.sa_handler = SigWinchHandler; Sa.sa_flags = SA_RESTART; sigaction(SIGWINCH, &Sa, &m_OldSigAction); } ~RawModeGuard() { if (m_Valid) { tcsetattr(STDIN_FILENO, TCSANOW, &m_OldAttrs); } sigaction(SIGWINCH, &m_OldSigAction, nullptr); } bool IsValid() const { return m_Valid; } private: struct termios m_OldAttrs = {}; struct sigaction m_OldSigAction = {}; 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; static char s_LastChar = 0; #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; } // Compute visible width of a string, skipping ANSI escape sequences. static int VisibleWidth(const std::string& Text) { int Width = 0; bool InEscape = false; for (char c : Text) { if (InEscape) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { InEscape = false; // Final byte of escape sequence } } else if (c == '\033') { InEscape = true; } else { ++Width; } } return Width; } // Truncate a string that may contain ANSI escape sequences to a visible width. // Appends "..." if truncated. Ensures ANSI state is reset after truncation. static std::string TruncateAnsi(const std::string& Text, int MaxVisible) { if (MaxVisible <= 0) { return {}; } int Visible = 0; bool InEscape = false; for (size_t i = 0; i < Text.size(); ++i) { char c = Text[i]; if (InEscape) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { InEscape = false; } } else if (c == '\033') { InEscape = true; } else { ++Visible; if (Visible >= MaxVisible - 2) // Leave room for "..." { return Text.substr(0, i + 1) + "\033[0m..."; } } } return Text; } // Word-wrap a single line to fit within Width visible columns. // ANSI escape codes pass through without counting toward width. // Returns one or more lines. static std::vector WrapLine(const std::string& Line, uint32_t Width) { if (Width == 0) { return {Line}; } // Fast path: line already fits if (VisibleWidth(Line) <= static_cast(Width)) { return {Line}; } // Measure leading whitespace for continuation indent so wrapped lines // align with the original text. int IndentChars = 0; for (char c : Line) { if (c == ' ') { ++IndentChars; } else if (c == '\t') { IndentChars += 4; } else { break; } } // Cap indent to half the width to avoid degenerate cases int ContinuationIndent = std::min(IndentChars, static_cast(Width) / 2); std::vector Result; std::string CurrentLine; int CurrentVisible = 0; int EffectiveWidth = static_cast(Width); bool InEscape = false; bool HasWords = false; // True once a word has been appended to CurrentLine // Track the current "word" being accumulated std::string Word; int WordVisible = 0; auto FlushWord = [&]() { if (Word.empty()) { return; } // Would this word overflow? int SpaceNeeded = WordVisible; if (HasWords) { SpaceNeeded += 1; // space before word } if (HasWords && CurrentVisible + SpaceNeeded > EffectiveWidth) { // Wrap: emit current line, start new continuation line Result.push_back(CurrentLine); CurrentLine = std::string(ContinuationIndent, ' '); CurrentVisible = ContinuationIndent; EffectiveWidth = static_cast(Width); HasWords = false; } // Force-break words wider than the available space if (WordVisible > EffectiveWidth - CurrentVisible) { // Append character by character bool WordEscape = false; for (char c : Word) { if (WordEscape) { CurrentLine += c; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { WordEscape = false; } } else if (c == '\033') { CurrentLine += c; WordEscape = true; } else { if (CurrentVisible >= EffectiveWidth) { Result.push_back(CurrentLine); CurrentLine = std::string(ContinuationIndent, ' '); CurrentVisible = ContinuationIndent; HasWords = false; } CurrentLine += c; ++CurrentVisible; } } HasWords = true; Word.clear(); WordVisible = 0; return; } // Append with space separator if (HasWords) { CurrentLine += ' '; ++CurrentVisible; } CurrentLine += Word; CurrentVisible += WordVisible; HasWords = true; Word.clear(); WordVisible = 0; }; for (size_t i = 0; i < Line.size(); ++i) { char c = Line[i]; if (InEscape) { Word += c; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { InEscape = false; } } else if (c == '\033') { Word += c; InEscape = true; } else if (c == ' ' || c == '\t') { FlushWord(); } else { Word += c; ++WordVisible; } } FlushWord(); if (!CurrentLine.empty() || Result.empty()) { Result.push_back(CurrentLine); } return Result; } std::vector TuiWrapLines(const std::vector& Lines, uint32_t Width) { std::vector Result; for (const std::string& Line : Lines) { std::vector Wrapped = WrapLine(Line, Width); Result.insert(Result.end(), Wrapped.begin(), Wrapped.end()); } return Result; } // Case-insensitive substring match static bool ContainsCaseInsensitive(const std::string& Haystack, const std::string& Needle) { if (Needle.empty()) { return true; } auto It = std::search(Haystack.begin(), Haystack.end(), Needle.begin(), Needle.end(), [](char A, char B) { return std::tolower(static_cast(A)) == std::tolower(static_cast(B)); }); return It != Haystack.end(); } int TuiPickOne(std::string_view Title, std::span Items, int InitialSelection, std::string* InOutFilter, std::span SearchTexts) { TuiEnterAlternateScreen(); const int TotalCount = static_cast(Items.size()); constexpr int kIndicatorLen = 3; // " ▶ " or " " — 3 display columns // When SearchTexts is provided, filter against it instead of display labels bool UseSearchTexts = !SearchTexts.empty(); // Filter state — seed from caller if provided std::string Filter = InOutFilter ? *InOutFilter : std::string{}; std::vector Visible; // Indices into Items that match the current filter int CursorPos = 0; // Index into Visible int ScrollTop = 0; // First visible index in the viewport auto RebuildVisible = [&] { Visible.clear(); for (int i = 0; i < TotalCount; ++i) { const std::string& Haystack = (UseSearchTexts && i < static_cast(SearchTexts.size())) ? SearchTexts[i] : Items[i]; if (ContainsCaseInsensitive(Haystack, Filter)) { Visible.push_back(i); } } CursorPos = 0; ScrollTop = 0; }; RebuildVisible(); // Apply initial selection: find it in the Visible list if (InitialSelection > 0 && InitialSelection < TotalCount) { for (int i = 0; i < static_cast(Visible.size()); ++i) { if (Visible[i] == InitialSelection) { CursorPos = i; break; } } } // Layout: Row 1 = title bar, Row 2 = filter (optional), rows 3..N-1 = items, row N = hint footer // kFixedRows = title bar (1) + hint footer (1) = 2 constexpr int kFixedRows = 2; auto GetPageSize = [&]() -> int { int Rows = static_cast(TuiConsoleRows()); int FilterOverhead = Filter.empty() ? 0 : 1; int Available = Rows - kFixedRows - FilterOverhead; return std::max(Available, 1); }; auto EnsureCursorVisible = [&] { int PageSize = GetPageSize(); int VisibleCount = static_cast(Visible.size()); if (CursorPos < ScrollTop) { ScrollTop = CursorPos; } else if (CursorPos >= ScrollTop + PageSize) { ScrollTop = CursorPos - PageSize + 1; } int MaxScroll = std::max(0, VisibleCount - PageSize); ScrollTop = std::clamp(ScrollTop, 0, MaxScroll); }; auto Render = [&] { uint32_t Cols = TuiConsoleColumns(); uint32_t Rows = TuiConsoleRows(); int MaxTextLen = static_cast(Cols) - kIndicatorLen; int VisibleCount = static_cast(Visible.size()); int PageSize = GetPageSize(); TuiCursorHome(); // Row 1: Title bar (reverse video) TuiMoveCursor(1, 1); TuiEraseLine(); printf("\033[1;7m"); // bold + reverse printf(" %.*s", static_cast(std::min(static_cast(Title.size()), Cols - 1)), Title.data()); printf("\033[0m"); uint32_t CurrentRow = 2; // Optional filter bar if (!Filter.empty()) { TuiMoveCursor(CurrentRow, 1); TuiEraseLine(); printf(" \033[33mfilter:\033[0m %s", Filter.c_str()); if (VisibleCount == 0) { printf(" \033[2m(no matches)\033[0m"); } ++CurrentRow; } // Item viewport int ViewEnd = std::min(ScrollTop + PageSize, VisibleCount); for (int Row = 0; Row < PageSize; ++Row) { int i = ScrollTop + Row; TuiMoveCursor(CurrentRow + static_cast(Row), 1); TuiEraseLine(); if (i < VisibleCount) { bool IsSelected = (i == CursorPos); if (IsSelected) { printf("\033[1;7m"); } const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " "; const std::string& ItemText = Items[Visible[i]]; int ItemVisible = VisibleWidth(ItemText); if (MaxTextLen > 0 && ItemVisible > MaxTextLen) { printf("%s%s", Indicator, TruncateAnsi(ItemText, MaxTextLen).c_str()); } else { printf("%s%s", Indicator, ItemText.c_str()); } if (IsSelected) { printf("\033[0m"); } } } // Hint footer (last row) TuiMoveCursor(Rows, 1); TuiEraseLine(); printf("\033[7m"); printf( " \xe2\x86\x91/\xe2\x86\x93 navigate " "Enter confirm " "Esc %s " "Type to filter", Filter.empty() ? "cancel" : "clear"); if (ScrollTop > 0 || ViewEnd < VisibleCount) { printf(" [%d-%d of %d]", ScrollTop + 1, ViewEnd, VisibleCount); } printf("\033[0m"); TuiFlush(); }; EnsureCursorVisible(); Render(); int Result = -1; bool Done = false; while (!Done) { ConsoleKey Key = TuiReadKey(); int VisibleCount = static_cast(Visible.size()); switch (Key) { case ConsoleKey::ArrowUp: if (VisibleCount > 0) { CursorPos = (CursorPos - 1 + VisibleCount) % VisibleCount; EnsureCursorVisible(); } break; case ConsoleKey::ArrowDown: if (VisibleCount > 0) { CursorPos = (CursorPos + 1) % VisibleCount; EnsureCursorVisible(); } break; case ConsoleKey::PageUp: if (VisibleCount > 0) { CursorPos = std::max(0, CursorPos - GetPageSize()); EnsureCursorVisible(); } break; case ConsoleKey::PageDown: if (VisibleCount > 0) { CursorPos = std::min(VisibleCount - 1, CursorPos + GetPageSize()); EnsureCursorVisible(); } break; case ConsoleKey::Enter: if (VisibleCount > 0) { Result = Visible[CursorPos]; } Done = true; break; case ConsoleKey::Escape: if (!Filter.empty()) { Filter.clear(); RebuildVisible(); EnsureCursorVisible(); } else { Done = true; } break; case ConsoleKey::Backspace: if (!Filter.empty()) { Filter.pop_back(); RebuildVisible(); EnsureCursorVisible(); } break; case ConsoleKey::Char: Filter += TuiReadKeyChar(); RebuildVisible(); EnsureCursorVisible(); break; case ConsoleKey::Resize: EnsureCursorVisible(); break; default: break; } Render(); } // Persist filter state for the caller if (InOutFilter) { *InOutFilter = std::move(Filter); } TuiExitAlternateScreen(); 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 // Enable window-size events so ReadConsoleInput returns WINDOW_BUFFER_SIZE_EVENT { HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); DWORD dwMode = 0; if (GetConsoleMode(hStdin, &dwMode)) { SetConsoleMode(hStdin, dwMode | ENABLE_WINDOW_INPUT); } } #else 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; } } // Install SIGWINCH handler for terminal resize detection s_GotSigWinch = 0; struct sigaction Sa = {}; Sa.sa_handler = SigWinchHandler; Sa.sa_flags = SA_RESTART; sigaction(SIGWINCH, &Sa, nullptr); #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; } // Restore default SIGWINCH handler signal(SIGWINCH, SIG_DFL); #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 } void TuiSetScrollRegion(uint32_t Top, uint32_t Bottom) { printf("\033[%u;%ur", Top, Bottom); } void TuiResetScrollRegion() { printf("\033[r"); } void TuiMoveCursor(uint32_t Row, uint32_t Col) { printf("\033[%u;%uH", Row, Col); } void TuiSaveCursor() { printf( "\033" "7"); } void TuiRestoreCursor() { printf( "\033" "8"); } void TuiEraseLine() { printf("\033[2K"); } void TuiWrite(std::string_view Text) { fwrite(Text.data(), 1, Text.size(), stdout); } void TuiFlush() { fflush(stdout); } void TuiShowCursor(bool Show) { if (Show) { printf("\033[?25h"); } else { printf("\033[?25l"); } } ////////////////////////////////////////////////////////////////////////// // Public key reading ConsoleKey TuiReadKey() { #if ZEN_PLATFORM_WINDOWS HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); INPUT_RECORD Record{}; DWORD dwRead = 0; while (true) { if (!ReadConsoleInputA(hStdin, &Record, 1, &dwRead)) { return ConsoleKey::Escape; } if (Record.EventType == WINDOW_BUFFER_SIZE_EVENT) { return ConsoleKey::Resize; } if (Record.EventType != KEY_EVENT || !Record.Event.KeyEvent.bKeyDown) { continue; } switch (Record.Event.KeyEvent.wVirtualKeyCode) { case VK_UP: return ConsoleKey::ArrowUp; case VK_DOWN: return ConsoleKey::ArrowDown; case VK_LEFT: return ConsoleKey::ArrowLeft; case VK_RIGHT: return ConsoleKey::ArrowRight; case VK_PRIOR: return ConsoleKey::PageUp; case VK_NEXT: return ConsoleKey::PageDown; case VK_HOME: return ConsoleKey::Home; case VK_END: return ConsoleKey::End; case VK_RETURN: return ConsoleKey::Enter; case VK_ESCAPE: return ConsoleKey::Escape; case VK_BACK: return ConsoleKey::Backspace; default: { char ch = Record.Event.KeyEvent.uChar.AsciiChar; if (ch >= 32 && ch < 127) { s_LastChar = ch; return ConsoleKey::Char; } break; } } } #else // Check for pending SIGWINCH before blocking on read if (s_GotSigWinch) { s_GotSigWinch = 0; return ConsoleKey::Resize; } unsigned char c = 0; if (read(STDIN_FILENO, &c, 1) != 1) { // read() returns -1 with EINTR when interrupted by SIGWINCH if (errno == EINTR && s_GotSigWinch) { s_GotSigWinch = 0; return ConsoleKey::Resize; } return ConsoleKey::Escape; } if (c == 27) // ESC or escape sequence { int Next = ReadByteWithTimeout(50); if (Next == '[') { int Code = ReadByteWithTimeout(50); switch (Code) { case 'A': return ConsoleKey::ArrowUp; case 'B': return ConsoleKey::ArrowDown; case 'C': return ConsoleKey::ArrowRight; case 'D': return ConsoleKey::ArrowLeft; case 'H': return ConsoleKey::Home; case 'F': return ConsoleKey::End; case '5': if (ReadByteWithTimeout(50) == '~') { return ConsoleKey::PageUp; } break; case '6': if (ReadByteWithTimeout(50) == '~') { return ConsoleKey::PageDown; } break; default: break; } } return ConsoleKey::Escape; } if (c == '\r' || c == '\n') { return ConsoleKey::Enter; } if (c == 127 || c == 8) { return ConsoleKey::Backspace; } if (c >= 32 && c < 127) { s_LastChar = c; return ConsoleKey::Char; } return ConsoleKey::Unknown; #endif } char TuiReadKeyChar() { return s_LastChar; } ////////////////////////////////////////////////////////////////////////// // TuiPager — fullscreen scrollable text viewer with search and word wrapping void TuiPager(std::string_view Title, const std::vector& Lines) { TuiEnterAlternateScreen(); // Word-wrapped lines and the last width they were wrapped to std::vector Wrapped; uint32_t WrapWidth = 0; auto Rewrap = [&]() { uint32_t Cols = TuiConsoleColumns(); if (Cols != WrapWidth) { Wrapped = TuiWrapLines(Lines, Cols); WrapWidth = Cols; } }; Rewrap(); uint32_t TopLine = 0; // Search state std::string SearchQuery; int32_t SearchMatchLine = -1; bool SearchFailed = false; bool SearchWrapped = false; auto GetPageHeight = [&]() -> uint32_t { uint32_t Rows = TuiConsoleRows(); return (Rows > 2) ? (Rows - 2) : 1; }; auto ClampTop = [&]() { uint32_t PageH = GetPageHeight(); uint32_t LineCount = static_cast(Wrapped.size()); if (LineCount <= PageH) { TopLine = 0; } else if (TopLine > LineCount - PageH) { TopLine = LineCount - PageH; } }; auto Render = [&]() { uint32_t Cols = TuiConsoleColumns(); uint32_t Rows = TuiConsoleRows(); uint32_t PageH = GetPageHeight(); uint32_t LineCount = static_cast(Wrapped.size()); TuiCursorHome(); // Title bar TuiMoveCursor(1, 1); printf("\033[1;7m"); TuiEraseLine(); uint32_t LastVisible = std::min(TopLine + PageH, LineCount); printf(" %.*s (lines %u-%u of %u)", static_cast(std::min(static_cast(Title.size()), Cols - 30)), Title.data(), LineCount > 0 ? TopLine + 1 : 0, LastVisible, LineCount); printf("\033[0m"); // Content lines for (uint32_t i = 0; i < PageH; ++i) { TuiMoveCursor(i + 2, 1); TuiEraseLine(); uint32_t LineIdx = TopLine + i; if (LineIdx < LineCount) { const std::string& Line = Wrapped[LineIdx]; if (SearchMatchLine >= 0 && LineIdx == static_cast(SearchMatchLine)) { printf("\033[43;30m"); } printf("%s", Line.c_str()); if (SearchMatchLine >= 0 && LineIdx == static_cast(SearchMatchLine)) { printf("\033[0m"); } } } // Status bar TuiMoveCursor(Rows, 1); TuiEraseLine(); printf("\033[7m"); if (LineCount <= PageH) { printf(" (All)"); } else if (TopLine == 0) { printf(" (Top)"); } else if (TopLine + PageH >= LineCount) { printf(" (End)"); } else { uint32_t Pct = (TopLine * 100) / (LineCount - PageH); printf(" (%u%%)", Pct); } if (!SearchQuery.empty()) { if (SearchFailed) { printf(" \033[0;7;31mno match:\033[0;7m %s", SearchQuery.c_str()); } else if (SearchWrapped) { printf(" search (wrapped): %s", SearchQuery.c_str()); } else { printf(" search: %s", SearchQuery.c_str()); } } else { printf(" Esc:quit Type to search Enter:next match"); } printf("\033[0m"); TuiFlush(); }; auto FindNext = [&](uint32_t StartLine, bool Wrap) -> bool { if (SearchQuery.empty()) { return false; } uint32_t LineCount = static_cast(Wrapped.size()); for (uint32_t i = 0; i < LineCount; ++i) { uint32_t Idx = (StartLine + i) % LineCount; if (Idx < StartLine && !Wrap) { break; } if (ContainsCaseInsensitive(Wrapped[Idx], SearchQuery)) { SearchMatchLine = static_cast(Idx); SearchFailed = false; SearchWrapped = Wrap && (Idx < StartLine); uint32_t PageH = GetPageHeight(); if (static_cast(SearchMatchLine) < TopLine || static_cast(SearchMatchLine) >= TopLine + PageH) { TopLine = static_cast(SearchMatchLine); if (TopLine > 3) { TopLine -= 3; } else { TopLine = 0; } ClampTop(); } return true; } } SearchFailed = true; SearchWrapped = false; SearchMatchLine = -1; return false; }; ClampTop(); Render(); bool Done = false; while (!Done) { ConsoleKey Key = TuiReadKey(); SearchFailed = false; SearchWrapped = false; uint32_t PageH = GetPageHeight(); uint32_t LineCount = static_cast(Wrapped.size()); switch (Key) { case ConsoleKey::ArrowUp: if (TopLine > 0) { --TopLine; } break; case ConsoleKey::ArrowDown: if (TopLine + PageH < LineCount) { ++TopLine; } break; case ConsoleKey::PageUp: if (TopLine >= PageH) { TopLine -= PageH; } else { TopLine = 0; } break; case ConsoleKey::PageDown: TopLine += PageH; ClampTop(); break; case ConsoleKey::Home: TopLine = 0; break; case ConsoleKey::End: if (LineCount > PageH) { TopLine = LineCount - PageH; } break; case ConsoleKey::Char: SearchQuery += TuiReadKeyChar(); FindNext(TopLine, true); break; case ConsoleKey::Backspace: if (!SearchQuery.empty()) { SearchQuery.pop_back(); if (!SearchQuery.empty()) { FindNext(TopLine, true); } else { SearchMatchLine = -1; } } break; case ConsoleKey::Enter: { uint32_t Start = (SearchMatchLine >= 0) ? static_cast(SearchMatchLine) + 1 : TopLine; FindNext(Start, true); break; } case ConsoleKey::Escape: if (!SearchQuery.empty()) { SearchQuery.clear(); SearchMatchLine = -1; } else { Done = true; } break; case ConsoleKey::Resize: Rewrap(); ClampTop(); break; default: break; } Render(); } TuiExitAlternateScreen(); } } // namespace zen