AuroraRuntime/Source/Console/ConsoleTTY/ConsoleTTY.cpp

1963 lines
49 KiB
C++

/***
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 <Source/RuntimeInternal.hpp>
#include <Source/Console/Console.hpp>
#include "ConsoleTTY.hpp"
#include <Source/Console/ConsoleStd/ConsoleStd.hpp>
#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->HistoryAppendChanges();
}
bool TTYConsole::Start()
{
gTTYConsoleEnabled = true;
#if defined(AURORA_IS_MODERNNT_DERIVED)
this->oldCP = GetConsoleOutputCP();
SetConsoleOutputCP(CP_UTF8);
#endif
if (NoncanonicalMode())
{
ConsoleStd::EnterNoncanonicalMode();
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (IsWin32UxMode())
{
UXModeStart();
}
#endif
return true;
}
void TTYConsole::End()
{
gTTYConsoleEnabled = false;
TTYClearScreen();
TTYClearLine(EAnsiColor::eReset);
if (NoncanonicalMode())
{
ConsoleStd::LeaveNoncanonicalMode();
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
SetConsoleOutputCP(this->oldCP);
#endif
if (!UTF8())
{
TTYWrite("\033(B");
}
}
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)
{
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 TTYConsole::NoncanonicalOnEnter()
{
AU_LOCK_GUARD(this->historyLock->AsWritable());
if (this->inputField.size())
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (this->inputField == "!s")
{
DWORD mode;
HANDLE stream = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(stream, &mode);
mode &= ~(ENABLE_QUICK_EDIT_MODE);
SetConsoleMode(stream, mode);
uxModeFlipped = true;
}
else if (this->inputField == "!c")
{
DWORD mode;
HANDLE stream = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(stream, &mode);
mode |= ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(stream, mode);
uxModeFlipped = true;
}
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 (this->inputField == "help")
{
AuLogInfo("ConsoleTTY: !t = goto top, !b = goto bottom and lock-scroll");
}
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<AuUInt32>(this->noncanonicalCursorPos, GuessWidth(this->inputField));
this->noncanonicalCursorPosInBytes = AuMin<AuUInt32>(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<AuUInt32>(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<AuUInt64>(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<AuConsole::ConsoleMessage> &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', itr);
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<AuUInt>(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)
{
for (int i = startIndex + maxLines; i < this->currentHeight; i++)
{
this->BlankLine(i);
}
TTYScrollBuffer(delta);
for (int i = indexBeforePadding; i < startIndex; i++)
{
this->BlankLine(i);
}
forceRedrawIfFalse = true;
drawPos = startIndex + maxLines - delta;
}
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);
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();
BlankBordersLine(Y);
}
void TTYConsole::WriteBuffered(AuPair<AuUInt32, AuUInt32> 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 true;// false;
}
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<AuUInt32, AuUInt32> TTYConsole::GetLogBoxStart()
{
return {this->GetLeftBorder(), this->GetTopBorder() + this->GetBannerLines() + this->GetBannerFootBorder()};
}
AuPair<AuUInt32, AuUInt32> TTYConsole::GetHintLine()
{
int hintLines = this->GetHintLines();
return {this->GetLeftBorder(), this->currentHeight - (1 + hintLines + bool(hintLines) + this->GetBottomBorder() + this->GetTextInputLines())};
}
AuPair<AuUInt32, AuUInt32> 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<AuUInt32, AuUInt32> TTYConsole::GetInitialInputLine()
{
return {};
}
void TTYConsole::NoncanonicalSetCursor()
{
TTYSetPos(this->GetInputCursor());
}
void TTYConsole::SetHintStrings(const AuList<AuPair<AuString /*autocompletion*/, AuList<AuString/* secondary hint*/>>> &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::GetPaddingHeadOfLog(AuUInt8 newValue)
{
return AuExchange(this->topLogPadding, newValue);
}
AuUInt8 TTYConsole::GetPaddingTopOfLog(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<ITTYConsole> GetTTYConsole()
{
return AuUnsafeRaiiToShared<ITTYConsole>(&gTTYConsole);
}
void OnEnter()
{
if (gTTYConsoleEnabled)
{
gTTYConsole.OnEnter();
}
}
#else
void Init()
{
}
void Pump()
{
}
void Exit()
{
}
void WriteTTYOut(const AuConsole::ConsoleMessage &msg)
{
}
AUKN_SYM AuSPtr<ITTYConsole> GetTTYConsole()
{
return {};
}
void OnEnter()
{
}
#endif
}