/*** Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: ConsoleTTY.cpp Date: 2022-5-11 Author: Reece ***/ #define I_REALLY_NEED_WIDECHAR_PUBAPI // bc linux #include #include #include "ConsoleTTY.hpp" #include #if defined(AURORA_IS_MODERNNT_DERIVED) #include "ConsoleTTY.NT.hpp" #endif #if defined(AURORA_IS_POSIX_DERIVED) #include "ConsoleTTY.Unix.hpp" #endif namespace Aurora::Console::ConsoleTTY { #if 1 bool TTYConsole::IsShowingHintLine() { return this->inputField.size(); } void TTYConsole::Init() { this->historyLock = AuThreadPrimitives::RWLockUnique(); this->HistorySetFile(); this->HistoryLoad(); } void TTYConsole::Deinit() { this->End(); this->HistoryAppendChanges(); } bool TTYConsole::Start() { if (!ConsoleStd::IsStdOutTTY()) { return false; } gTTYConsoleEnabled = true; #if defined(AURORA_IS_MODERNNT_DERIVED) this->oldCP = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); #endif #if defined(AURORA_IS_POSIX_DERIVED) TTYWrite("\033[?1049h"); #endif if (NoncanonicalMode()) { ConsoleStd::EnterNoncanonicalMode(); } #if defined(AURORA_IS_MODERNNT_DERIVED) if (IsWin32UxMode()) { UXModeStart(); } #endif return true; } void TTYConsole::End() { if (!AuExchange(gTTYConsoleEnabled, false)) { return; } if (NoncanonicalMode()) { ConsoleStd::LeaveNoncanonicalMode(); } TTYClearScreen(); TTYClearLine(EAnsiColor::eReset); #if defined(AURORA_IS_POSIX_DERIVED) TTYWrite("\033[?1049l"); #endif #if defined(AURORA_IS_MODERNNT_DERIVED) SetConsoleOutputCP(this->oldCP); #endif if (!UTF8()) { TTYWrite("\033(B"); } #if defined(AURORA_IS_POSIX_DERIVED) ConsoleStd::Unlock(); // just really making sure #endif ConsoleStd::Flush(); } void TTYConsole::BufferMessage(const AuConsole::ConsoleMessage &msg) { AU_LOCK_GUARD(this->messageLock); AuTryInsert(this->messagesPending, msg); } void TTYConsole::NoncanonicalTick() { ConsoleStd::NoncanonicalTick(); auto inputs = ConsoleStd::DequeueNoncanonicalInput(); if (inputs.size() && PermitDoubleBuffering()) { BeginBuffering(); } for (auto &input : inputs) { //AuLogDbg("[HANDLE] Key Stroke: {}, {}, {}. control: {}, alt: {}, shift: {}", ConsoleStd::ENoncanonicalInputToString(input.type), input.scrollDeltaY, input.string, input.isControlSequence, input.isAltSequence, input.isShiftSequence); switch (input.type) { case ConsoleStd::ENoncanonicalInput::eInput: { NoncanonicalOnString(input.string); break; } case ConsoleStd::ENoncanonicalInput::eBackspace: { NoncanonicalOnBackspace(); break; } case ConsoleStd::ENoncanonicalInput::eEnter: { NoncanonicalOnEnter(); break; } case ConsoleStd::ENoncanonicalInput::eArrowLeft: { NoncanonicalOnLeft(); break; } case ConsoleStd::ENoncanonicalInput::eArrowRight: { NoncanonicalOnRight(); break; } case ConsoleStd::ENoncanonicalInput::ePageDown: { NoncanonicalOnPageDown(); break; } case ConsoleStd::ENoncanonicalInput::ePageUp: { NoncanonicalOnPageUp(); break; } case ConsoleStd::ENoncanonicalInput::eArrowDown: { if (this->noncanonicalCursorPosInBytes == 0 || this->GetHintLines() == 0 || this->noncanonicalCursorPosInBytes == this->inputField.size()) { NoncanonicalOnHistoryDown(); } else { NoncanonicalOnMenuDown(); } break; } case ConsoleStd::ENoncanonicalInput::eArrowUp: { if (this->noncanonicalCursorPosInBytes == 0 || this->GetHintLines() == 0 || this->noncanonicalCursorPosInBytes == this->inputField.size()) { NoncanonicalOnHistoryUp(); } else { NoncanonicalOnMenuUp(); } break; } case ConsoleStd::ENoncanonicalInput::eScroll: { Scroll(input.scrollDeltaY); break; } default: { AuLogDbg("Key Stroke: {}, {}, {}", ConsoleStd::ENoncanonicalInputToString(input.type), input.scrollDeltaY, input.string); } }; } if (inputs.size() && PermitDoubleBuffering()) { EndBuffering(); } } void TTYConsole::NoncanonicalOnString(const AuString &input) { if (this->noncanonicalCursorPosInBytes == this->inputField.size()) { this->inputField += input; } else { auto itr = this->inputField.begin(); std::advance(itr, this->noncanonicalCursorPosInBytes); this->inputField.insert(itr, input.begin(), input.end()); } this->noncanonicalCursorPos += GuessWidth(input); this->noncanonicalCursorPosInBytes += (int)input.size(); RedrawInput(true); NoncanonicalSetCursor(); } void TTYConsole::NoncanonicalOnLeft() { if (this->noncanonicalCursorPosInBytes <= 0) { if (this->noncanonicalCursorPosInBytes == -1) { NoncanonicalOnMenuLeft(); } return; } int idx = AuLocale::Encoding::CountUTF8Length({this->inputField.data(), AuUInt(this->noncanonicalCursorPosInBytes - 1)}, true); this->noncanonicalCursorPos--; this->noncanonicalCursorPosInBytes = idx; NoncanonicalSetCursor(); } void TTYConsole::NoncanonicalOnRight() { if (this->noncanonicalCursorPosInBytes == -1) { NoncanonicalOnMenuRight(); return; } if (this->noncanonicalCursorPosInBytes == this->inputField.size()) { return; } // TODO: missing locale API this->noncanonicalCursorPos++; this->noncanonicalCursorPosInBytes++; // << ILLEGAL NoncanonicalSetCursor(); } void TTYConsole::NoncanonicalOnBackspace() { if (noncanonicalCursorPosInBytes <= 0) { return; } int idx = AuLocale::Encoding::CountUTF8Length({this->inputField.data(), AuUInt(this->noncanonicalCursorPosInBytes - 1)}, true); int endCnt = AuLocale::Encoding::CountUTF8Length({this->inputField.data() + idx, this->inputField.size() - idx}, true); if (!endCnt) { this->inputField.resize(idx); } else { auto suffix = inputField.substr(noncanonicalCursorPosInBytes); this->inputField.resize(idx); this->inputField += suffix; } this->noncanonicalCursorPos--; this->noncanonicalCursorPosInBytes = idx; RedrawInput(true); NoncanonicalSetCursor(); } bool TTYConsole::IsWin32UxMode() { return true; } void TTYConsole::UXModeStart() { #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD mode; HANDLE stream = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(stream, &mode); mode |= ENABLE_QUICK_EDIT_MODE; SetConsoleMode(stream, mode); #endif } void TTYConsole::UXModeFlip() { if (AuExchange(uxModeFlipped, true)) { return; } if (!IsWin32UxMode()) { return; } #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD mode; HANDLE stream = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(stream, &mode); mode &= ~(ENABLE_QUICK_EDIT_MODE); SetConsoleMode(stream, mode); #endif } void EnterScrollMode() { #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD mode; HANDLE stream = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(stream, &mode); mode &= ~(ENABLE_QUICK_EDIT_MODE); SetConsoleMode(stream, mode); AuStaticCast(GetTTYConsole())->uxModeFlipped = true; #endif } void LeaveScrollMode() { #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD mode; HANDLE stream = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(stream, &mode); mode |= ENABLE_QUICK_EDIT_MODE; SetConsoleMode(stream, mode); AuStaticCast(GetTTYConsole())->uxModeFlipped = true; // TODO? #endif } void TTYConsole::NoncanonicalOnEnter() { AU_LOCK_GUARD(this->historyLock->AsWritable()); if (this->inputField.size()) { #if defined(AURORA_IS_MODERNNT_DERIVED) if (this->inputField == "!s") { EnterScrollMode(); } else if (this->inputField == "!c") { LeaveScrollMode(); } else #endif if (this->inputField == "!b") { this->iScrollPos = -1; this->bTriggerRedraw = true; } else if (this->inputField == "!t") { this->iScrollPos = 0; this->bTriggerRedraw = true; } else { #if defined(AURORA_IS_MODERNNT_DERIVED) if (this->inputField == "help") { AuLogInfo("ConsoleTTY: Type !s to enter scroll mode, type !c to enter host-os controlled copy/paste mode (quick-edit)"); } #endif #if defined(AURORA_IS_POSIX_DERIVED) if (this->inputField == "help") { AuLogInfo("ConsoleTTY: Hold control + arrow key up/down to scroll."); } #endif if (this->inputField == "help") { AuLogInfo("ConsoleTTY: use the command !t to scroll to the top, the command !b to lock-scroll to the bottom."); } AuConsole::DispatchRawLine(this->inputField); } } AuTryInsert(this->history, this->inputField); this->noncanonicalCursorPos = 0; this->noncanonicalCursorPosInBytes = 0; this->inputField.clear(); OnEnter(); } void TTYConsole::NoncanonicalOnMenuLeft() { } void TTYConsole::NoncanonicalOnMenuRight() { } void TTYConsole::NoncanonicalOnMenuUp() { } void TTYConsole::NoncanonicalOnMenuDown() { } void TTYConsole::NoncanonicalOnHistoryUp() { bool locked {}; if (this->history.empty()) { return; } if (this->noncanonicalCursorPos == 0 || noncanonicalCursorPosInBytes == this->inputField.size()) { locked = true; } if (this->iHistoryPos == -1) { this->iHistoryPos = this->history.size(); } this->iHistoryPos--; if (this->iHistoryPos < 0) { this->iHistoryPos = 0; } this->inputField = this->history[this->iHistoryPos]; HistoryUpdateCursor(); if (locked) { this->noncanonicalCursorPos = GuessWidth(this->inputField); this->noncanonicalCursorPosInBytes = (int)this->inputField.size(); NoncanonicalSetCursor(); } } void TTYConsole::NoncanonicalOnHistoryDown() { AU_LOCK_GUARD(this->historyLock->AsReadable()); bool locked {}; if (this->history.empty()) { return; } if (this->noncanonicalCursorPos == 0 || noncanonicalCursorPosInBytes == this->inputField.size()) { locked = true; } if (this->iHistoryPos == -1) { return; } this->iHistoryPos++; if (this->iHistoryPos >= this->history.size()) { this->iHistoryPos = -1; this->inputField.clear(); } else { this->inputField = this->history[this->iHistoryPos]; } HistoryUpdateCursor(); if (locked) { this->noncanonicalCursorPos = GuessWidth(this->inputField); this->noncanonicalCursorPosInBytes = (int)this->inputField.size(); NoncanonicalSetCursor(); } } void TTYConsole::HistoryUpdateCursor() { this->noncanonicalCursorPos = AuMin(this->noncanonicalCursorPos, GuessWidth(this->inputField)); this->noncanonicalCursorPosInBytes = AuMin(this->noncanonicalCursorPosInBytes, AuUInt32(this->inputField.size())); RedrawInput(true); NoncanonicalSetCursor(); } void TTYConsole::HistorySetFile() { AuString path; AuProcess::GetProcFullPath(path); auto hash = AuFnv1a64Runtime(path.data(), path.size()); AuIOFS::NormalizePath(this->historyFileName, fmt::format("~/TTYHistory/{}.txt", hash)); } AuString TTYConsole::HistoryGetFile() { return this->historyFileName; } void TTYConsole::HistoryAppendChanges() { if (!this->historyLock) { return; } AU_LOCK_GUARD(this->historyLock->AsWritable()); if (this->history.size() <= this->iHistoryWritePos) { return; } auto file = AuIOFS::OpenUnique(this->historyFileName, AuIOFS::EFileOpenMode::eReadWrite); if (!file) { SysPushErrorIO(this->historyFileName); return; } file->SetOffset(file->GetLength()); AuString buffer; auto line = AuLocale::NewLine(); line.reserve(4096); for (int i = this->iHistoryWritePos; i < this->history.size(); i++) { buffer += (i == 0 ? "" : line); buffer += this->history[i]; } AuUInt bytesWritten; file->Write(AuMemoryViewStreamRead(AuMemory::MemoryViewRead(buffer), bytesWritten)); this->iHistoryWritePos = this->history.size(); } void TTYConsole::HistoryLoad() { AU_LOCK_GUARD(this->historyLock->AsWritable()); if (this->historyFileName.empty()) { return; } if (!AuIOFS::FileExists(this->historyFileName)) { return; } AuString buffer; if (!AuIOFS::ReadString(this->historyFileName, buffer)) { return; } AuParse::SplitNewlines(buffer, [&](const AuString &in) { this->history.push_back(in); }); this->iHistoryWritePos = this->history.size(); } void TTYConsole::NoncanonicalOnPageUp() { this->Scroll(this->GetLogBoxLines()); } void TTYConsole::NoncanonicalOnPageDown() { this->Scroll(-this->GetLogBoxLines()); } void TTYConsole::Scroll(int delta) { auto maxLines = GetLogBoxLines(); if (this->screenBuffer.size() <= maxLines) { this->iScrollPos = -1; return; } if (this->iScrollPos == -1) { this->iScrollPos = AuMax(this->screenBuffer.size() - maxLines, 0); } this->iScrollPos -= delta; if (this->iScrollPos < 0) { this->iScrollPos = 0; } if (this->screenBuffer.size() - maxLines < this->iScrollPos ) { this->iScrollPos = -1; } this->bTriggerRedraw = true; } void TTYConsole::PumpHistory() { static Aurora::Utility::RateLimiter limiter; if (!limiter.nextTriggerTime) { limiter.noCatchUp = true; limiter.SetNextStep(AuMSToNS(10'000)); } if (!limiter.CheckExchangePass()) { return; } HistoryAppendChanges(); } void TTYConsole::Pump() { if (!gTTYConsoleEnabled) { return; } if (NoncanonicalMode()) { NoncanonicalTick(); } Flush(); PumpHistory(); } void TTYConsole::OnEnter() { this->iHistoryPos = -1; auto line = this->currentHeight; this->inputField.clear(); this->RedrawInput(true); this->NoncanonicalSetCursor(); } void TTYConsole::Flush() { AU_LOCK_GUARD(this->messageLock); RedrawWindow(); } bool TTYConsole::RegenerateBuffer(bool resChanged, bool &forceRedrawIfFalse) { auto messagesPending = AuExchange(this->messagesPending, {}); bool bShouldRegenerateStringArary {}; bool widthChanged = this->oldWidth != this->currentWidth; bool sizeChanged = widthChanged || this->currentHeight != this->oldHeight; // ew auto maxLines = GetLogBoxLines(); int maxWidth = this->currentWidth - int(GetRightBorder()) - int(GetLeftBorder()) - int(this->leftLogPadding) - this->rightLogPadding; // Word-wrap cancer if (this->bScreenBufferDoesntMap) { if (resChanged) { bShouldRegenerateStringArary = widthChanged; } } else { if (resChanged) { for (auto &message : this->screenBuffer) { int anyLineRemoveEsc = message.rfind('\x1b', 0) == 0; if (message.size() - (anyLineRemoveEsc * 7) > maxWidth) { bShouldRegenerateStringArary = true; break; } } } } // Unsafe insert this->messages.insert(this->messages.end(), messagesPending.begin(), messagesPending.end()); // Insert console message while attempting to preserve the intention of word-wrapping auto addMessages = [&](const AuList &messages) { for (auto &message : messages) { auto str = message.ToConsole(); while (str.size()) { int XOffset = GetLeftBorder() + this->leftLogPadding; auto itr = str.npos; itr = str.find('\t'); while (itr != str.npos) { auto suffix = str.substr(itr + 1); str = str.substr(0, itr); AuString padding(4 - (itr % 4), ' '); str += padding; str += suffix; itr = str.find('\t', itr); } int anyLineRemoveEsc = str.rfind('\x1b', 0) == 0; int idx = AuLocale::Encoding::CountUTF8Length({str.data(), AuMin(str.size(), maxWidth + (anyLineRemoveEsc * 7))}, true); auto append = str.substr(0, idx); AuTryInsert(this->screenBuffer, append); str = str.substr(idx); this->bScreenBufferDoesntMap |= bool(str.size()); } } if (this->screenBuffer.size() > maxLines) { UXModeFlip(); } }; if (bShouldRegenerateStringArary) { // Recalculate our AuString array of lines this->screenBuffer.clear(); addMessages(this->messages); // Redraw entire screenbuffer return true; } else { // Fast path - just append the damn changes int oldSize = this->screenBuffer.size(); addMessages(messagesPending); auto bannerLines = this->GetBannerLines(); auto indexBeforePadding = this->GetTopBorder() + this->GetBannerFootBorder() + (bannerLines ? this->topLogPadding + bannerLines : 0) ; auto startIndex = this->topLogPaddingExtra + indexBeforePadding; int drawPos = {}; auto delta = this->screenBuffer.size() - oldSize; if (iScrollPos == -1) { if (this->screenBuffer.size() > maxLines) { // TODO (Reece): Removing optimization bc it was broken. This is a nightmare. auto indexBeforePadding = (this->GetBannerFootBorder() ? 1 + this->topLogPadding : 0) + this->topLogPaddingExtra; auto startIndex = this->GetTopBorder() + this->GetBannerLines() + indexBeforePadding; for (int i = startIndex; i < this->currentHeight; i++) { this->BlankLine(i); } this->screenBuffer.clear(); addMessages(this->messages); return true; } else { drawPos = startIndex + oldSize; } } else { return false; } if (delta >= this->GetLogBoxLines()) { return true; } else { for (int i = 0; i < delta; i++) { WriteBuffered({GetLeftBorder() + this->leftLogPadding, i + drawPos}, this->screenBuffer[i + oldSize]); } } return false || resChanged; } } bool TTYConsole::UTF8() { #if defined(AURORA_IS_MODERNNT_DERIVED) return true; #else return AuLocale::GetLocale().codepage == AuLocale::ECodePage::eUTF8; #endif } bool TTYConsole::RedrawWindow() { bool bTryAgain {}; bool bRedrawn = {}; bool bRefresh {}; do { bTryAgain = false; bool bWarmed {}; #if defined(AURORA_IS_MODERNNT_DERIVED) bWarmed = WarmBuffering(); #endif auto pos = TTYScreenSize(); bool bChangedWidth = currentWidth != pos.first; bool bChangedHeight = currentHeight != pos.second; bool bChangedSize = bChangedWidth || bChangedHeight || bWarmed; bool bRedrawWindowRequired = this->bTriggerRedraw; bool bHasMesasgesPending = messagesPending.size() || this->bTriggerRedraw; auto currentHash = AuFnv1a64Runtime(inputField.data(), inputField.size()); bool bInputChanged = NoncanonicalMode() && (lastInputHash != currentHash && lastInputHash != 0); lastInputHash = currentHash; oldWidth = currentWidth; oldHeight = currentHeight; currentWidth = pos.first; currentHeight = pos.second; if (currentWidth < 0) { bTryAgain = true; continue; } bool redrawEntireBox {}; if (bChangedSize || bRedrawWindowRequired || bHasMesasgesPending) { bRedrawn = true; if (PermitDoubleBuffering()) { BeginBuffering(); } if (NoncanonicalMode()) { } else { TTYStorePos(); } if (bChangedSize || this->bTriggerRedraw) { TTYClearScreen(); bRefresh = true; redrawEntireBox = true; } } if (bChangedSize || bHasMesasgesPending) { bool redrawBox {}; bRefresh = bChangedSize || this->bTriggerRedraw; if (RegenerateBuffer(bChangedSize, redrawBox) || this->bTriggerRedraw) { bRefresh = true; redrawEntireBox = true; RedrawLogBox(); } bRefresh |= redrawBox; } if (redrawEntireBox) { RedrawLogBox(); } if (bRefresh) { RedrawBanner(); RedrawBorders(); } if (bRedrawn || bRefresh) { RedrawInput(NoncanonicalMode()); this->NoncanonicalSetCursor(); TTYStorePos(); } if (bRedrawn || bRefresh) { RedrawHintLine(); } #if 0 DebugLogArea(); #endif if (bRedrawn) { if (NoncanonicalMode()) { NoncanonicalSetCursor(); } else { TTYRestorePos(); } // Account for race conditions of size-change during draw if (PermitDoubleBuffering()) { bTryAgain = !EndBuffering(); } } this->bTriggerRedraw = false; } while (bTryAgain && false); return bRedrawn; } void TTYConsole::DebugLogArea() { auto lines = this->GetLogBoxLines(); auto start = this->GetTopBorder() + (this->GetBannerLines() ? this->GetBannerLines() + 1 : 0) + (this->GetBannerFootBorder() ? this->topLogPadding : 0) + this->topLogPaddingExtra; AuString blank(this->currentWidth - GetRightBorder() - GetLeftBorder() - this->leftLogPadding - this->rightLogPadding, '~'); for (int i = 0; i < lines; i++) { WriteBuffered({GetLeftBorder() + this->leftLogPadding, i + start}, blank); } } void TTYConsole::WriteLine(int Y, const AuString &in) { #if defined(AURORA_IS_MODERNNT_DERIVED) RecordFunction(std::bind(&TTYConsole::WriteLine, this, Y, std::string(in))); #endif if (in.empty()) { return; } int XOffset {}; XOffset = GetLeftBorder(); TTYSetPos({XOffset, Y}); #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD idc; WriteConsoleA(GetTTYHandle(), in.data(), AuUInt32(in.size()), &idc, NULL); #else ConsoleStd::WriteStdOutBlocking2(in.data(), in.size()); #endif } void TTYConsole::BlankBordersLine(int Y) { AuString bar; if (UTF8()) { bar = "\xe2\x94\x82"; } else { bar = "\033(0x\033(B"; } WriteBuffered({0, Y}, bar); WriteBuffered({this->currentWidth - 1, Y}, bar); } void TTYConsole::BlankLine(int Y, bool borders) { TTYSetPos({0, Y}); TTYClearLine(EAnsiColor::eReset); BlankBordersLine(Y); } void TTYConsole::WriteBuffered(AuPair pos, const AuString &in) { #if defined(AURORA_IS_MODERNNT_DERIVED) RecordFunction(std::bind(&TTYConsole::WriteBuffered, this, pos, std::string(in))); #endif if (in.empty()) { return; } TTYSetPos(pos); #if defined(AURORA_IS_MODERNNT_DERIVED) DWORD idc; WriteConsoleA(GetTTYHandle(), in.data(), AuUInt32(in.size()), &idc, NULL); #else ConsoleStd::WriteStdOutBlocking2(in.data(), in.size()); #endif } void TTYConsole::RedrawBorders() { TTYSetPos({}); AuString start; AuString end; AuString bar; AuString startLine; AuString midLine; AuString endLine; startLine.clear(); if (!UTF8()) { start = "\033(0"; end = "\033(B"; for (int i = 0; i < currentWidth; i++) { startLine += "q"; } midLine = endLine = startLine; if (GetLeftBorder()) { startLine[0] = 'l'; } if (GetRightBorder()) { startLine[startLine.size() - 1] = 'k'; } if (GetLeftBorder()) { midLine[0] = 't'; } if (GetRightBorder()) { midLine[midLine.size() - 1] = 'u'; } if (GetLeftBorder()) { endLine[0] = 'm'; } if (GetRightBorder()) { endLine[endLine.size() - 1] = 'j'; } bar = 'x'; } else { AuString slash = "\xe2\x94\x80"; for (int i = GetLeftBorder(); i < currentWidth - int(GetRightBorder()); i++) { startLine += slash; } midLine = endLine = startLine; if (GetLeftBorder()) { startLine = "\xe2\x94\x8c" + startLine; } if (GetRightBorder()) { startLine += "\xe2\x94\x90"; } if (GetLeftBorder()) { midLine = "\xe2\x94\x9c" + midLine; } if (GetRightBorder()) { midLine += "\xe2\x94\xa4"; } if (GetLeftBorder()) { endLine = "\xe2\x94\x94" + endLine; } if (GetRightBorder()) { endLine += "\xe2\x94\x98"; } bar = "\xe2\x94\x82"; } int height = currentHeight; int widthM = currentWidth - 1; // Begin drawing WriteBuffered({}, start); // Box: top if (GetTopBorder()) { WriteBuffered({}, Stringify(startLine, this->headerTitle, true, true, true)); } // Box: sides for (int i = GetTopBorder(); i < height - GetBottomBorder(); i++) { WriteBuffered({0, i}, bar); WriteBuffered({widthM, i}, bar); } // Box: Bottom if (GetBottomBorder()) { WriteBuffered({0, height - 1}, endLine); } // Above hint splitter line if (GetLogBoxHintBorder()) { int l = this->GetHintLines(); // GetLogBoxHintBorder WriteBuffered({0, this->currentHeight - (this->GetBottomBorder() + this->GetTextInputLines() + this->topInputPadding + this->GetLogBoxHintBorder() + (l ? (this->topHintPadding) : 0))}, midLine); } // Below subhead splitter line if (this->GetBannerFootBorder()) { int l = this->GetBannerLines(); if (l) { WriteBuffered({0, this->GetTopBorder() + this->GetBannerLines()}, Stringify(midLine, this->logTitle, true, true, true)); } } // End drawing mode WriteBuffered({}, end); } AuUInt32 TTYConsole::GuessWidth(const AuString &referenceLine) { return AuUInt32(AuLocale::ConvertFromUTF8(referenceLine).size()); } const AuString &TTYConsole::Stringify(const AuString &referenceLine, const AlignedString &string, bool spacing, bool brackets, bool extraPadding) { if (string.str.empty()) { return referenceLine; } this->tempMemory = referenceLine; int lineWidth = this->currentWidth; int stride = 1; if (!referenceLine.empty()) { if (UTF8()) { stride = 3; } } // HACK: if non-title, remove borders // Before i'd drop the first & last char if line == width, but ofc this doesnt work with UTF8, and it'd pretty dumb to rely on an estimated char length int L {}, R {}; if (!extraPadding) { lineWidth -= GetLeftBorder() * stride; lineWidth -= GetRightBorder() * stride; } else { // Border offsets, if accounting for them. L = GetLeftBorder(); R = GetRightBorder(); } if (lineWidth < 0) { this->tempMemory.clear(); return this->tempMemory; } if (referenceLine.empty()) { this->tempMemory = AuString(lineWidth, ' '); } int length = GuessWidth(string.str) + (brackets ? 2 : 0) + (spacing ? 2 : 0); if (length > lineWidth) { return referenceLine; } int start {}; int padding2 = extraPadding ? 2 : 0; // You're an idiot. you dont need a comment to work out what this does switch (string.align) { case ETTYAlign::eLeft: start = padding2 + L; break; case ETTYAlign::eRight: start = lineWidth - (R + padding2 + length); break; case ETTYAlign::eCenter: start = lineWidth / 2 - (length / 2); break; } if (start + length + GetRightBorder() > this->currentWidth) { return referenceLine; } // In UTF8 mode, we fill the buffer of characters of a constant stride of 3 start *= stride; if (start >= tempMemory.size()) { return referenceLine; } // Brackets AuString left; AuString right; if (brackets) { if (UTF8()) { right = "\xe2\x94\x9c"; left = "\xe2\x94\xa4"; } else { left = "u"; right = "t"; } } // Overhead to enter DEC drawing mode around the brackets on non-utf8 terminals int DECOverhead = 0; int DECOverheadA = 0; int DECOverheadB = 0; if (!UTF8()) { DECOverheadA = DECOverheadB = 3; DECOverhead = DECOverheadA + DECOverheadB; } // kanker AuUInt toWrite = (brackets * stride) + DECOverheadA + spacing + string.str.size() + spacing + DECOverheadB + (brackets * stride); AuUInt toSlice = stride * ((spacing * 2) + (brackets * 2) + string.str.size()); // brrr if (toWrite <= toSlice) { // Fast path, pog. We dont need to allocate. AuUInt strideTwo = brackets * stride; if (toWrite < toSlice) { AuUInt second = start + strideTwo + DECOverheadA + spacing + string.str.size() + spacing + DECOverheadB + right.size(); AuUInt two = start + (length * strideTwo); AuUInt count = this->tempMemory.size() - two; AuMemmove(this->tempMemory.data() + second, this->tempMemory.data() + two, this->tempMemory.size() - two); this->tempMemory.resize(second + count); } if (brackets) { AuMemcpy(this->tempMemory.data() + start, left.data(), left.size()); } if (spacing) { this->tempMemory.data()[start + strideTwo + DECOverheadA] = ' '; } if (DECOverheadA) { AuMemcpy(this->tempMemory.data() + strideTwo + DECOverheadA + spacing, "\033(B", 3); } AuMemcpy(this->tempMemory.data() + start + strideTwo + DECOverheadA + spacing, string.str.data(), string.str.size()); if (spacing) { this->tempMemory.data()[start + strideTwo + DECOverheadA + spacing + string.str.size() + DECOverheadB] = ' '; } if (DECOverheadB) { AuMemcpy(this->tempMemory.data() + start + strideTwo + DECOverheadA + spacing + string.str.size() + spacing, "\033(0", 3); } if (brackets) { AuMemcpy(this->tempMemory.data() + start + strideTwo + DECOverheadA + spacing + string.str.size() + spacing + DECOverheadB, right.data(), right.size()); } } else { // Slow allocation path auto cpy = this->tempMemory; // alloc AuString &ret = this->tempMemory; auto begin = cpy.empty() ? "" : cpy.substr(0, start); // alloc auto end = cpy.empty() ? "" : cpy.substr(start + (length * stride)); // alloc ret = AuMove(begin); ret.reserve(this->currentWidth * stride); // potential allocs: if (brackets) { ret += left; } if (DECOverheadA) { ret += "\033(B"; } if (spacing) { ret += ' '; } ret += string.str; if (spacing) { ret += ' '; } if (DECOverheadB) { ret += "\033(0"; } if (brackets) { ret += right; } ret += AuMove(end); return ret; } return this->tempMemory; } void TTYConsole::RedrawBanner() { if (this->header.str.size()) { WriteLine(this->GetTopBorder() + this->topHeaderPadding, Stringify("", this->header, false, false, false)); } if (this->subheader.str.size()) { WriteLine(this->GetTopBorder() + (this->header.str.size() ? 1 + this->topHeaderPadding : 0) + (this->midHeaderPadding), Stringify("", this->subheader, false, false, false)); } } void TTYConsole::RedrawLogBox() { auto indexBeforePadding = (this->GetBannerFootBorder() ? 1 + this->topLogPadding : 0) + this->topLogPaddingExtra; auto startIndex = this->GetTopBorder() + this->GetBannerLines() + indexBeforePadding; int drawPos = {}; auto maxLines = GetLogBoxLines(); for (int i = startIndex + maxLines; i < this->currentHeight; i++) { this->BlankLine(i); } auto startingLineBufferIndex = 0; if (iScrollPos == -1) { if (this->screenBuffer.size() > maxLines) { startingLineBufferIndex = this->screenBuffer.size() - maxLines; } else { startingLineBufferIndex = 0; } } else { startingLineBufferIndex = iScrollPos; } for (int i = 0; i < maxLines; i++) { auto strIdx = i + startingLineBufferIndex; if (strIdx >= this->screenBuffer.size()) { break; } WriteBuffered({GetLeftBorder() + this->leftLogPadding, i + startIndex}, this->screenBuffer[strIdx]); } } void TTYConsole::BlankLogBox() { } void TTYConsole::RedrawInput(bool clear) { int height = this->currentHeight - (1 + this->GetBottomBorder()); if (clear) { BlankLine(height); } WriteBuffered({this->GetLeftBorder() + this->leftPadding, height}, ">"); if (clear && NoncanonicalMode()) { WriteBuffered({this->GetLeftBorder() + this->leftPadding + 1, height}, this->inputField); } } void TTYConsole::RedrawHintLine() { } int TTYConsole::GetHintLines() { return this->hintStrings.size(); } bool TTYConsole::GetLogBoxHintBorder() { return true; } bool TTYConsole::GetBottomBorder() { return true; } ETTYAlign TTYConsole::GetTitleAlignment() { return this->headerTitle.align; } ETTYAlign TTYConsole::SetTitleAlignment(ETTYAlign newValue) { return AuExchange(this->headerTitle.align, newValue); } ETTYAlign TTYConsole::GetLogBoxTitleAlignment() { return this->logTitle.align; } ETTYAlign TTYConsole::SetLogBoxTitleAlignment(ETTYAlign newValue) { return AuExchange(this->logTitle.align, newValue); } ETTYAlign TTYConsole::GetHeaderAlignment() { return this->header.align; } ETTYAlign TTYConsole::GetSubheaderAlignment() { return this->subheader.align; } AuString TTYConsole::GetHeaderBorderTitle() { return this->headerTitle.str; } AuString TTYConsole::GetLogBoxBorderTitle() { return this->logTitle.str; } AuString TTYConsole::GetHeader() { return this->header.str; } AuString TTYConsole::GetSubHeader() { return this->subheader.str; } ETTYAlign TTYConsole::SetHeaderAlignment(ETTYAlign newValue) { this->bTriggerRedraw = true; return AuExchange(this->header.align, newValue); } ETTYAlign TTYConsole::SetSubheaderAlignment(ETTYAlign newValue) { this->bTriggerRedraw = true; return AuExchange(this->subheader.align, newValue); } AuString TTYConsole::SetHeaderBorderTitle(const AuString &newValue) { this->bTriggerRedraw = true; return AuExchange(this->headerTitle.str, newValue); } AuString TTYConsole::SetLogBoxBorderTitle(const AuString &newValue) { this->bTriggerRedraw = true; return AuExchange(this->logTitle.str, newValue); } AuString TTYConsole::SetHeader(const AuString &newValue) { this->bTriggerRedraw = true; return AuExchange(this->header.str, newValue); } AuString TTYConsole::SetSubHeader(const AuString &newValue) { this->bTriggerRedraw = true; return AuExchange(this->subheader.str, newValue); } bool TTYConsole::SetTopBorder(bool newValue) { this->bTriggerRedraw = true; return AuExchange(this->bTopBorder, newValue); } bool TTYConsole::SetLeftBorder(bool newValue) { this->bTriggerRedraw = true; return AuExchange(this->bLeftBorder, newValue); } bool TTYConsole::SetRightBorder(bool newValue) { this->bTriggerRedraw = true; return AuExchange(this->bRightBorder, newValue); } int TTYConsole::GetBannerLines() { int ret = !this->header.str.empty() + !this->subheader.str.empty(); if (!ret) { return 0; } return ret + this->midHeaderPadding + (this->header.str.size() ? this->topHeaderPadding : 0) + (this->subheader.str.size() ? this->bottomSubHeaderPadding : 0); } int TTYConsole::GetLogBoxLines() { int hintLines = this->GetHintLines(); int bannerLines = this->GetBannerLines(); // return this->currentHeight - (this->GetTopBorder() + ////////////////// bannerLines + ////////////////// (bannerLines ? 1 /* splitter */ + this->topLogPadding : 0) + this->topLogPaddingExtra + GetLogBoxHintBorder() + ////////////////////////////// 1 + (bool(hintLines) ? (hintLines + this->topHintPadding) : 0) + this->topInputPadding + ////////////////////////////// this->GetTextInputLines() + ////////////////////////////// this->GetBottomBorder()); } int TTYConsole::GetTextInputLines() { return 1; } bool TTYConsole::GetBannerFootBorder() { return this->header.str.size() || this->subheader.str.size(); } bool TTYConsole::GetTopBorder() { return this->bTopBorder; } bool TTYConsole::GetRightBorder() { return this->bRightBorder; } bool TTYConsole::GetLeftBorder() { return this->bLeftBorder; } bool TTYConsole::NoncanonicalMode() { return ConsoleStd::IsStdOutTTY(); } bool TTYConsole::PermitDoubleBuffering() { // We can't double buffer and not share the input line between two framebuffers #if defined(AURORA_IS_MODERNNT_DERIVED) return NoncanonicalMode(); #endif return true; } AuPair TTYConsole::GetLogBoxStart() { return {this->GetLeftBorder(), this->GetTopBorder() + this->GetBannerLines() + this->GetBannerFootBorder()}; } AuPair TTYConsole::GetHintLine() { int hintLines = this->GetHintLines(); return {this->GetLeftBorder(), this->currentHeight - (1 + hintLines + bool(hintLines) + this->GetBottomBorder() + this->GetTextInputLines())}; } AuPair TTYConsole::GetInputCursor() { if (!this->NoncanonicalMode()) { return {this->GetLeftBorder() + this->leftPadding + 1 + this->leftInputPadding, this->currentHeight - (1 + this->GetBottomBorder())}; } return {this->GetLeftBorder() + this->leftPadding + 1 + this->leftInputPadding + (noncanonicalCursorPos == -1 ? 0 : noncanonicalCursorPos) /*:(*/, this->currentHeight - (1 + this->GetBottomBorder())}; // { i do not like this, ...} } AuPair TTYConsole::GetInitialInputLine() { return {}; } void TTYConsole::NoncanonicalSetCursor() { TTYSetPos(this->GetInputCursor()); } void TTYConsole::SetHintStrings(const AuList>> &hints) { this->hintStrings = AuMove(hints); } bool TTYConsole::CheckRedraw() { return {}; } void TTYConsole::SetInitialCursorPos() { } void TTYConsole::PostRedraw(bool first) { if (first) { SetInitialCursorPos(); //AuConsole::TTYSetPos() } else if (NoncanonicalMode()) { } } AuUInt8 TTYConsole::SetPaddingLeftOfLog(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->leftLogPadding, newValue); } AuUInt8 TTYConsole::SetPaddingRightOfLog(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->rightLogPadding, newValue); } AuUInt8 TTYConsole::SetPaddingLeftOfInput(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->leftInputPadding, newValue); } AuUInt8 TTYConsole::SetPaddingTopOfInput(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->topInputPadding, newValue); } AuUInt8 TTYConsole::SetPaddingTopOfHint(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->topHintPadding, newValue); } AuUInt8 TTYConsole::SetPaddingTopOfHeader(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->topHeaderPadding, newValue); } AuUInt8 TTYConsole::SetPaddingMidOfHeader(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->midHeaderPadding, newValue); } AuUInt8 TTYConsole::SetPaddingBottomOfSubheader(AuUInt8 newValue) { this->bTriggerRedraw = true; return AuExchange(this->bottomSubHeaderPadding, newValue); } AuUInt8 TTYConsole::GetPaddingLeftOfLog() { return this->leftLogPadding; } AuUInt8 TTYConsole::GetPaddingRightOfLog() { return this->rightLogPadding; } AuUInt8 TTYConsole::GetPaddingLeftOfInput() { return this->leftInputPadding; } AuUInt8 TTYConsole::GetPaddingTopOfInput() { return this->topInputPadding; } AuUInt8 TTYConsole::GetPaddingTopOfHint() { return this->topHintPadding; } AuUInt8 TTYConsole::GetPaddingTopOfHeader() { return this->topHeaderPadding; } AuUInt8 TTYConsole::GetPaddingMidOfHeader() { return this->midHeaderPadding; } AuUInt8 TTYConsole::GetPaddingBottomOfSubheader() { return this->bottomSubHeaderPadding; } AuUInt8 TTYConsole::SetPaddingHeadOfLog(AuUInt8 newValue) { return AuExchange(this->topLogPadding, newValue); } AuUInt8 TTYConsole::SetPaddingTopOfLog(AuUInt8 newValue) { return AuExchange(this->topLogPaddingExtra, newValue); } AuUInt8 TTYConsole::GetPaddingHeadOfLog() { return this->topLogPadding; } AuUInt8 TTYConsole::GetPaddingTopOfLog() { return this->topLogPaddingExtra; } void Init() { gTTYConsole.Init(); } void Exit() { gTTYConsole.Deinit(); } void WriteTTYOut(const AuConsole::ConsoleMessage &msg) { gTTYConsole.BufferMessage(msg); } void Pump() { gTTYConsole.Pump(); } AUKN_SYM AuSPtr GetTTYConsole() { return AuUnsafeRaiiToShared(&gTTYConsole); } void OnEnter() { if (gTTYConsoleEnabled) { gTTYConsole.OnEnter(); } } #else void Init() { } void Pump() { } void Exit() { } void WriteTTYOut(const AuConsole::ConsoleMessage &msg) { } AUKN_SYM AuSPtr GetTTYConsole() { return {}; } void OnEnter() { } #endif }