Merge branch 'development' into wrap-overhaul

This commit is contained in:
Harrison 2025-06-04 12:46:51 -07:00 committed by GitHub
commit e6c516e3ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1918 additions and 1356 deletions

View file

@ -1266,7 +1266,7 @@ void Host::send(QString cmd, bool wantPrint, bool dontExpandAliases)
// allow sending blank commands
if (!dontExpandAliases && commandList.empty()) {
if (commandList.empty()) {
QString payload(QChar::LineFeed);
mTelnet.sendData(payload);
return;
@ -4395,3 +4395,15 @@ void Host::editorThemeChanged()
{
emit signal_editorThemeChanged();
}
void Host::sendCmdLine(const QString& cmd)
{
if (!mpConsole || !mpConsole->mpCommandLine) {
qWarning() << "Host::sendCmdLine(...) ERROR - No active command line available.";
return;
}
// Set the command in the active command line
mpConsole->mpCommandLine->setPlainText(cmd);
mpConsole->mpCommandLine->selectAll();
}

View file

@ -433,6 +433,8 @@ public:
}
}
void sendCmdLine(const QString& cmd);
cTelnet mTelnet;
QPointer<TMainConsole> mpConsole;
QPointer<dlgPackageManager> mpPackageManager;

View file

@ -823,7 +823,7 @@ COMMIT_LINE:
lineBuffer << QString();
}
buffer.push_back(mMudBuffer);
timeBuffer << QTime::currentTime().toString(csmTimeStampFormat);
timeBuffer << QTime::currentTime().toString(mudlet::smTimeStampFormat);
if (ch == '\xff') {
promptBuffer.append(true);
} else {
@ -840,7 +840,7 @@ COMMIT_LINE:
lineBuffer.back().append(QString());
}
buffer.back() = mMudBuffer;
timeBuffer.back() = QTime::currentTime().toString(csmTimeStampFormat);
timeBuffer.back() = QTime::currentTime().toString(mudlet::smTimeStampFormat);
if (ch == '\xff') {
promptBuffer.back() = true;
} else {
@ -948,6 +948,11 @@ COMMIT_LINE:
TChar c((!mIsDefaultColor && mBold) ? mForeGroundColorLight : mForeGroundColor, mBackGroundColor, attributeFlags);
if (mHyperlinkActive) {
c.mLinkIndex = mCurrentHyperlinkLinkId;
c.mFlags |= TChar::Underline;
}
if (mpHost->mMxpClient.isInLinkMode()) {
c.mLinkIndex = mLinkStore.getCurrentLinkID();
c.mFlags |= TChar::Underline;
@ -2158,6 +2163,86 @@ void TBuffer::decodeOSC(const QString& sequence)
resetColors();
}
break;
case static_cast<quint8>('8'): {
// Handle OSC 8 hyperlinks in the form: "8;params;URI"
#if defined(DEBUG_OSC_PROCESSING)
qDebug().noquote() << "[OSC 8] Raw sequence: " << sequence;
qDebug().noquote() << "[OSC 8] Raw hex: " << sequence.toUtf8().toHex(' ');
#endif
QStringView rest = QStringView(sequence).mid(1); // skip selector "8;"
int firstSemi = rest.indexOf(';');
if (firstSemi == -1) {
qWarning() << "OSC 8: Missing first semicolon";
return;
}
int secondSemi = rest.indexOf(';', firstSemi + 1);
if (secondSemi == -1) {
qWarning() << "OSC 8: Missing second semicolon";
return;
}
QString param = rest.left(firstSemi).toString();
#if defined(DEBUG_OSC_PROCESSING)
if (!param.isEmpty()) {
qDebug().noquote().nospace() << "[OSC 8] Params provided (not used by Mudlet but shown for debugging): \"" << param << "\"";
}
#endif
QString rawUrl = rest.mid(secondSemi + 1).toString();
// OSC 8 ;; closes the hyperlink
if ((param.isEmpty() && rawUrl.isEmpty())) {
mCurrentHyperlinkUrl.clear();
mCurrentHyperlinkCommand.clear();
mCurrentHyperlinkHint.clear();
mCurrentHyperlinkLinkId = 0;
mHyperlinkActive = false;
break;
}
if (!rawUrl.isEmpty()) {
if (rawUrl.length() > 2048) {
qWarning() << "TBuffer::decodeOSC(...) - Rejected hyperlink: URL too long:" << rawUrl;
return;
}
QStringList command;
QStringList hint;
if (rawUrl.startsWith("send:")) {
QString innerCommand = QUrl::fromPercentEncoding(rawUrl.mid(5).toUtf8());
command = { qsl("send([[%1]])").arg(innerCommand) };
hint = { qsl("%1: %2").arg(QObject::tr("Send"), innerCommand) };
mCurrentHyperlinkUrl = innerCommand;
} else if (rawUrl.startsWith("prompt:")) {
QString innerCommand = QUrl::fromPercentEncoding(rawUrl.mid(7).toUtf8());
command = { qsl("sendCmdLine([[%1]])").arg(innerCommand) };
hint = { qsl("%1: %2").arg(QObject::tr("Prompt"), innerCommand) };
mCurrentHyperlinkUrl = innerCommand;
} else {
QUrl qurl(rawUrl);
QString scheme = qurl.scheme().toLower();
if (scheme == "http" || scheme == "https" || scheme == "ftp") {
command = { qsl("openUrl([[%1]])").arg(rawUrl) };
hint = { qsl("%1: %2").arg(QObject::tr("Open browser to"), rawUrl) };
mCurrentHyperlinkUrl = rawUrl;
} else {
qWarning().noquote().nospace() << "TBuffer::decodeOSC(...) - Ignored untrusted or unsupported URI scheme: \"" << scheme << "\"";
return;
}
}
mCurrentHyperlinkCommand = command;
mCurrentHyperlinkHint = hint;
mCurrentHyperlinkLinkId = mLinkStore.addLinks(command, hint, mpHost, QVector<int>());
mHyperlinkActive = true;
}
break;
}
default:
qDebug().noquote().nospace() << "TBuffer::decodeOSC(\"" << sequence << "\") ERROR - Unhandled <OSC>?...<ST> code, Mudlet will ignore it.";
}
@ -2281,7 +2366,7 @@ void TBuffer::appendLine(const QString& text, const int sub_start, const int sub
const TChar styling(fgColor, bgColor, (mEchoingText ? (TChar::Echo | flags) : flags), linkID);
buffer.back().push_back(styling);
if (firstChar) {
timeBuffer.back() = QTime::currentTime().toString(csmTimeStampFormat);
timeBuffer.back() = QTime::currentTime().toString(mudlet::smTimeStampFormat);
firstChar = false;
}
}
@ -2292,7 +2377,7 @@ void TBuffer::appendEmptyLine()
std::deque<TChar> const newLine;
buffer.push_back(newLine);
lineBuffer.push_back(QString());
timeBuffer << QTime::currentTime().toString(csmTimeStampFormat);
timeBuffer << QTime::currentTime().toString(mudlet::smTimeStampFormat);
promptBuffer << false;
}
@ -2590,7 +2675,7 @@ void TBuffer::log(int fromLine, int toLine)
// This only handles a single line of logged text at a time:
linesToLog << bufferToHtml(mpHost->mIsLoggingTimestamps, i);
} else {
linesToLog << ((mpHost->mIsLoggingTimestamps && !timeBuffer.at(i).isEmpty()) ? timeBuffer.at(i).left(csmTimeStampFormat.length()) : QString()) % lineBuffer.at(i) % QChar::LineFeed;
linesToLog << ((mpHost->mIsLoggingTimestamps && !timeBuffer.at(i).isEmpty()) ? timeBuffer.at(i).left(mudlet::smTimeStampFormat.length()) : QString()) % lineBuffer.at(i) % QChar::LineFeed;
}
}
@ -2653,7 +2738,7 @@ int TBuffer::wrapLine(int startLine, int maxWidth, int indentSize, int hangingIn
const bool isPrompt = promptBuffer[i];
const QString lineText = lineBuffer[i];
// a blank timestamp indicates a wrapped line
const bool isNewline = (time != csmBlankTimeStamp);
const bool isNewline = (time != mudlet::smBlankTimeStamp);
QList<WrapInfo> lineBreaks = getWrapInfo(lineText, isNewline, maxWidth, indent, hangingIndent);
if (lineBreaks.isEmpty()) {
tempList.append(lineText);
@ -2698,7 +2783,7 @@ int TBuffer::wrapLine(int startLine, int maxWidth, int indentSize, int hangingIn
if (w.isNewline) {
timeList.append(time);
} else {
timeList.append(csmBlankTimeStamp);
timeList.append(mudlet::smBlankTimeStamp);
}
queue.push(newBufferLine);
promptList.append(isPrompt);
@ -2856,6 +2941,12 @@ bool TBuffer::replaceInLine(QPoint& P_begin, QPoint& P_end, const QString& with,
void TBuffer::clear()
{
mCurrentHyperlinkUrl.clear();
mCurrentHyperlinkCommand.clear();
mCurrentHyperlinkHint.clear();
mCurrentHyperlinkLinkId = 0;
mHyperlinkActive = false;
while (!buffer.empty()) {
if (!deleteLines(0, 0)) {
break;
@ -3147,7 +3238,7 @@ QString TBuffer::bufferToHtml(const bool showTimeStamp /*= false*/, const int ro
// we will NOT need a closing "</span>"
if (showTimeStamp && !timeBuffer.at(row).isEmpty()) {
// TODO: formatting according to TTextEdit.cpp: if( i2 < timeOffset ) - needs updating if we allow the colours to be user set:
s.append(qsl("<span style=\"color: rgb(200,150,0); background: rgb(22,22,22); \">%1").arg(timeBuffer.at(row).left(csmTimeStampFormat.length())));
s.append(qsl("<span style=\"color: rgb(200,150,0); background: rgb(22,22,22); \">%1").arg(timeBuffer.at(row).left(mudlet::smTimeStampFormat.length())));
// Set the current idea of what the formatting is so we can spot if it
// changes:
currentFgColor = QColor(200, 150, 0);

View file

@ -334,8 +334,6 @@ public:
int mCursorY = 0;
bool mEchoingText = false;
inline static const QString csmTimeStampFormat = qsl("hh:mm:ss.zzz ");
inline static const QString csmBlankTimeStamp = qsl("------------ ");
private:
inline QList<WrapInfo> getWrapInfo(const QString& lineText, bool isNewline, const int maxWidth, const int indent, const int hangingIndent);
@ -425,6 +423,13 @@ private:
QByteArray mEncoding;
QTextCodec* mMainIncomingCodec = nullptr;
// OSC 8 hyperlink tracking
QString mCurrentHyperlinkUrl;
QStringList mCurrentHyperlinkCommand;
QStringList mCurrentHyperlinkHint;
int mCurrentHyperlinkLinkId = 0;
bool mHyperlinkActive = false;
};
#ifndef QT_NO_DEBUG_STREAM

View file

@ -1553,3 +1553,4 @@ void TCommandLine::slot_saveHistory()
}
}
}

View file

@ -88,6 +88,7 @@ TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidge
// which has its own title and icon set.
setWindowTitle(tr("Debug Console"));
mWrapAt = 50;
mShowTimeStamps = true;
} else if (mType == MainConsole) {
mBorders = mpHost->borders();
mCommandBgColor = mpHost->mCommandBgColor;
@ -309,8 +310,10 @@ TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidge
timeStampButton->setIcon(QIcon(qsl(":/icons/dialog-information.png")));
timeStampButton->setToolTip(utils::richText(tr("Toggle time stamps")));
connect(timeStampButton, &QAbstractButton::toggled, mUpperPane, &TTextEdit::slot_toggleTimeStamps);
connect(timeStampButton, &QAbstractButton::toggled, mLowerPane, &TTextEdit::slot_toggleTimeStamps);
// Using the QAbstractButton::clicked rather than QAbstractButton::toggled
// so that we can set the state of the button without getting the signal
// being raised:
connect(timeStampButton, &QAbstractButton::clicked, this, &TConsole::slot_toggleTimeStamps);
replayButton = new QToolButton;
replayButton->setCheckable(true);
@ -2440,3 +2443,79 @@ void TConsole::clearSplit()
mUpperPane->updateScreenView();
mUpperPane->forceUpdate();
}
void TConsole::raiseMudletResizeEvent()
{
// Hiding the TConsole - particularly the main one, multiview is not active
// and the profile is being switched away from causes a zero column count
// even though the TConsole is not actually resized - so don't raise the
// Mudlet TEvent in that case:
auto characterDimensions = QSize(mUpperPane->getColumnCount(), mUpperPane->getRowCount());
if (!characterDimensions.width()) {
return;
}
// Showing, Hiding and then Showing the console will produce three resize
// events - whilst the prior step will prevent this method from generating
// and event for the hiding one the two successive showing ones will
// still get to here - so we also need to check that there HAS been an
// actual change in the dimensions - and abort if there hasn't:
if (mDimensions == characterDimensions) {
return;
}
mDimensions = characterDimensions;
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(qsl("sysConsoleSizeChanged"));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentList.append(QString::number(characterDimensions.width()));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentList.append(QString::number(characterDimensions.height()));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentList.append(QString::number(mShowTimeStamps ? mudlet::smTimeStampFormat.size() : 0));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mpHost->raiseEvent(mudletEvent);
}
void TConsole::slot_toggleTimeStamps(const bool state)
{
if (mShowTimeStamps == state) {
return;
}
mShowTimeStamps = state;
if (mType == TConsole::MainConsole) {
if (timeStampButton->isChecked() != state) {
// using this will NOT cause the QAbstractButton::checked signal
// to be raised - which is why we use that rather than the
// QAbstractButton::toggled one
timeStampButton->setChecked(state);
}
const auto filePath = mudlet::getMudletPath(enums::profileDataItemPath, mpHost->getName(), qsl("autotimestamp"));
QSaveFile file(filePath);
if (state) {
file.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream out(&file);
if (!file.commit()) {
qDebug() << "TConsole::slot_toggleTimeStamps: error saving timestamp state: " << file.errorString();
}
} else {
QFile::remove(filePath);
}
}
// These hardly do anything now - just forces a redraw
mUpperPane->toggleTimeStamps(state);
mLowerPane->toggleTimeStamps(state);
if (mpHost && mType == TConsole::MainConsole) {
// Update and send out the NAWS data:
mpHost->updateDisplayDimensions();
}
if (mType & (TConsole::MainConsole | TConsole::UserWindow | TConsole::SubConsole)) {
raiseMudletResizeEvent();
}
}

View file

@ -197,7 +197,6 @@ public:
void hideEvent(QHideEvent* event) override;
void setConsoleBgColor(int, int, int, int);
QColor getConsoleBgColor() const { return mBgColor; }
// Not used: void setConsoleFgColor(int, int, int);
std::list<int> getFgColor();
std::list<int> getBgColor();
@ -224,6 +223,8 @@ public:
// non-scrolling window:
void handleLinesOverflowEvent(const int lineCount);
void clearSplit();
bool showTimeStamps() const { return mShowTimeStamps; }
void raiseMudletResizeEvent();
QPointer<Host> mpHost;
@ -325,6 +326,7 @@ public slots:
void slot_toggleLogging();
void slot_changeControlCharacterHandling(const ControlCharacterMode);
void slot_toggleSearchCaseSensitivity(bool);
void slot_toggleTimeStamps(const bool);
signals:
void resized(QResizeEvent* event);
@ -355,6 +357,11 @@ private:
bool mF3SearchEnabled = false;
QPointer<QShortcut> mpSearchNextShortcut;
QPointer<QShortcut> mpSearchPrevShortcut;
// The size of the TConsole in (normal) "character" cells:
QSize mDimensions;
// Whether to show (a 13 character by default) timestamp to the left of
// each line of text:
bool mShowTimeStamps = false;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(TConsole::ConsoleType)

View file

@ -2960,6 +2960,16 @@ int TLuaInterpreter::expandAlias(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#sendCmdLine
int TLuaInterpreter::sendCmdLine(lua_State* L)
{
const QString text = getVerifiedString(L, __func__, 1, "command");
Host& host = getHostFromLua(L);
host.sendCmdLine(text);
lua_pushboolean(L, true);
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#send
// Note this is registered as send NOT sendRaw - see initLuaGlobals()
// It converts the bytes in the command (the first argument) from Utf-8 to be
@ -5070,6 +5080,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "killTrigger", TLuaInterpreter::killTrigger);
lua_register(pGlobalLua, "getLineCount", TLuaInterpreter::getLineCount);
lua_register(pGlobalLua, "getColumnNumber", TLuaInterpreter::getColumnNumber);
lua_register(pGlobalLua, "sendCmdLine", TLuaInterpreter::sendCmdLine);
lua_register(pGlobalLua, "send", TLuaInterpreter::sendRaw);
lua_register(pGlobalLua, "selectCaptureGroup", TLuaInterpreter::selectCaptureGroup);
lua_register(pGlobalLua, "tempLineTrigger", TLuaInterpreter::tempLineTrigger);
@ -5550,6 +5561,9 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "loadProfile", TLuaInterpreter::loadProfile);
lua_register(pGlobalLua, "closeProfile", TLuaInterpreter::closeProfile);
lua_register(pGlobalLua, "getCollisionLocationsInArea", TLuaInterpreter::getCollisionLocationsInArea);
lua_register(pGlobalLua, "disableTimeStamps", TLuaInterpreter::disableTimeStamps);
lua_register(pGlobalLua, "enableTimeStamps", TLuaInterpreter::enableTimeStamps);
lua_register(pGlobalLua, "timeStampsEnabled", TLuaInterpreter::timeStampsEnabled);
// PLACEMARKER: End of main Lua interpreter functions registration
// check new functions against https://www.linguistic-antipatterns.com when creating them

View file

@ -296,6 +296,7 @@ public:
static int feedTelnet(lua_State*);
static int Wait(lua_State*);
static int expandAlias(lua_State*);
static int sendCmdLine(lua_State*);
static int sendRaw(lua_State*);
static int echo(lua_State*);
static int selectString(lua_State*); // Was select but I think it clashes with the Lua command with that name
@ -690,6 +691,9 @@ public:
static int loadProfile(lua_State*);
static int closeProfile(lua_State*);
static int getCollisionLocationsInArea(lua_State*);
static int disableTimeStamps(lua_State*);
static int enableTimeStamps(lua_State*);
static int timeStampsEnabled(lua_State*);
// PLACEMARKER: End of Lua functions declarations
// check new functions against https://www.linguistic-antipatterns.com when creating them

View file

@ -543,6 +543,27 @@ int TLuaInterpreter::disableScrollBar(lua_State* L)
return 0;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#disableTimeStamps
int TLuaInterpreter::disableTimeStamps(lua_State* L)
{
const QString windowName {WINDOW_NAME(L, 1)};
auto pConsole = CONSOLE(L, windowName);
// *pConsole can be the main console as well as any user one
if (!pConsole->showTimeStamps()) {
lua_pushnil(L);
if (windowName.isEmpty()) {
lua_pushstring(L, qsl("timestamps were not enabled for the main console").toUtf8().constData());
} else {
lua_pushstring(L, qsl("timestamps were not enabled for the \"%1\" console").arg(windowName).toUtf8().constData());
}
return 2;
}
pConsole->slot_toggleTimeStamps(false);
lua_pushboolean(L, true);
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#echoLink
int TLuaInterpreter::echoLink(lua_State* L)
{
@ -717,6 +738,27 @@ int TLuaInterpreter::enableScrollBar(lua_State* L)
return 0;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#enableTimeStamps
int TLuaInterpreter::enableTimeStamps(lua_State* L)
{
const QString windowName {WINDOW_NAME(L, 1)};
auto pConsole = CONSOLE(L, windowName);
// *pConsole can be the main console as well as any user one
if (pConsole->showTimeStamps()) {
lua_pushnil(L);
if (windowName.isEmpty()) {
lua_pushstring(L, qsl("timestamps were not enabled for the main console").toUtf8().constData());
} else {
lua_pushstring(L, qsl("timestamps were not enabled for the \"%1\" console").arg(windowName).toUtf8().constData());
}
return 2;
}
pConsole->slot_toggleTimeStamps(true);
lua_pushboolean(L, true);
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getAvailableFonts
int TLuaInterpreter::getAvailableFonts(lua_State* L)
{
@ -1267,6 +1309,16 @@ int TLuaInterpreter::getTextFormat(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#timeStampsEnabled
int TLuaInterpreter::timeStampsEnabled(lua_State* L)
{
const QString windowName {WINDOW_NAME(L, 1)};
auto pConsole = CONSOLE(L, windowName);
// *pConsole can be the main console as well as any user one
lua_pushboolean(L, pConsole->showTimeStamps());
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getUserWindowSize
int TLuaInterpreter::getUserWindowSize(lua_State* L)
{

View file

@ -69,7 +69,6 @@ TTextEdit::TTextEdit(TConsole* pC, QWidget* pW, TBuffer* pB, Host* pH, bool isLo
, mOldCaretColumn(0)
, mIsCommandPopup(false)
, mIsTailMode(true)
, mShowTimeStamps(false)
, mForceUpdate(false)
, mIsLowerPane(isLowerPane)
, mLastRenderedOffset(0)
@ -83,9 +82,6 @@ TTextEdit::TTextEdit(TConsole* pC, QWidget* pW, TBuffer* pB, Host* pH, bool isLo
, mMaxHRange(0)
, mWideAmbigousWidthGlyphs(pH->wideAmbiguousEAsianGlyphs())
, mTabStopwidth(8)
// Should be the same as the size of the csmTimeStampFormat constant in the TBuffer
// class:
, mTimeStampWidth(13)
, mMouseWheelRemainder()
{
mLastClickTimer.start();
@ -108,7 +104,6 @@ TTextEdit::TTextEdit(TConsole* pC, QWidget* pW, TBuffer* pB, Host* pH, bool isLo
#endif
} else {
// This is part of the Central Debug Console
mShowTimeStamps = true;
mFontHeight = QFontMetrics(mDisplayFont).height();
mFontWidth = QFontMetrics(mDisplayFont).averageCharWidth();
mFgColor = QColor(192, 192, 192);
@ -177,30 +172,11 @@ void TTextEdit::focusOutEvent(QFocusEvent* event)
}
// debug using gammaray to see which events are raised
void TTextEdit::slot_toggleTimeStamps(const bool state)
void TTextEdit::toggleTimeStamps(const bool state)
{
if (mShowTimeStamps != state) {
mShowTimeStamps = state;
if (mpConsole->getType() == TConsole::MainConsole) {
const auto filePath = mudlet::getMudletPath(enums::profileDataItemPath, mpHost->getName(), qsl("autotimestamp"));
QSaveFile file(filePath);
if (state) {
file.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream out(&file);
if (!file.commit()) {
qDebug() << "TTextEdit::slot_toggleTimeStamps: error saving timestamp state: " << file.errorString();
}
} else {
QFile::remove(filePath);
}
}
forceUpdate();
update();
if (mpConsole->getType() == TConsole::MainConsole && mpConsole->mpHost) {
// Update and send out the NAWS data:
mpConsole->mpHost->updateDisplayDimensions();
}
}
Q_UNUSED(state)
forceUpdate();
update();
}
// Only wired up for the upper pane:
@ -458,7 +434,7 @@ void TTextEdit::drawLine(QPainter& painter, int lineNumber, int lineOfScreen, in
QString lineText = mpBuffer->lineBuffer.at(lineNumber);
QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Grapheme, lineText);
int currentSize = lineText.size();
if (mShowTimeStamps) {
if (mpConsole->showTimeStamps()) {
TChar timeStampStyle(QColor(200, 150, 0), QColor(22, 22, 22));
QString timestamp(mpBuffer->timeBuffer.at(lineNumber));
QVector<QColor> fgColors;
@ -476,7 +452,7 @@ void TTextEdit::drawLine(QPainter& painter, int lineNumber, int lineOfScreen, in
++index;
drawGraphemeForeground(painter, fgColors.at(index), textRects.at(index), c, timeStampStyle);
}
currentSize += mTimeStampWidth;
currentSize += mudlet::smTimeStampFormat.size();
}
//get the longest line
@ -1261,8 +1237,8 @@ int TTextEdit::convertMouseXToBufferX(const int mouseX, const int lineNumber, bo
// Do an additional check if we need to establish whether we are
// over just the timestamp part of the line:
if (Q_UNLIKELY(isOverTimeStamp && mShowTimeStamps && indexOfChar == 0)) {
if ((mouseX + offset) < (mTimeStampWidth * mFontWidth)) {
if (Q_UNLIKELY(isOverTimeStamp && mpConsole->showTimeStamps() && indexOfChar == 0)) {
if ((mouseX + offset) < (mudlet::smTimeStampFormat.size() * mFontWidth)) {
// The mouse position is actually over the timestamp region
// to the left of the main text:
*isOverTimeStamp = true;
@ -1272,8 +1248,8 @@ int TTextEdit::convertMouseXToBufferX(const int mouseX, const int lineNumber, bo
leftX = rightX;
//mCursorX relevant for horizontal scrollbars
//Otherwise the value is always 0
if (mShowTimeStamps) {
rightX = (mTimeStampWidth + column - mCursorX) * mFontWidth;
if (mpConsole->showTimeStamps()) {
rightX = (mudlet::smTimeStampFormat.size() + column - mCursorX) * mFontWidth;
} else {
rightX = (column - mCursorX) * mFontWidth;
}
@ -1347,7 +1323,7 @@ void TTextEdit::mousePressEvent(QMouseEvent* event)
}
bool isOutOfbounds = false;
if (!mCtrlSelecting && mShowTimeStamps) {
if (!mCtrlSelecting && mpConsole->showTimeStamps()) {
bool isOverTimeStamp = false;
x = convertMouseXToBufferX(eventPos.x(), y, &isOutOfbounds, &isOverTimeStamp);
if (isOverTimeStamp) {
@ -1576,14 +1552,14 @@ void TTextEdit::slot_copySelectionToClipboardHTML()
}
if (y == mPA.y()) { // First line of selection
if (isSingleLine) {
text.append(mpBuffer->bufferToHtml(mShowTimeStamps, y, mPB.x() + 1, mPA.x(), 0));
text.append(mpBuffer->bufferToHtml(mpConsole->showTimeStamps(), y, mPB.x() + 1, mPA.x(), 0));
} else { // Not single line
text.append(mpBuffer->bufferToHtml(mShowTimeStamps, y, -1, mPA.x(), mPA.x()));
text.append(mpBuffer->bufferToHtml(mpConsole->showTimeStamps(), y, -1, mPA.x(), mPA.x()));
}
} else if (y == mPB.y()) { // Last line of selection
text.append(mpBuffer->bufferToHtml(mShowTimeStamps, y, mPB.x() + 1));
text.append(mpBuffer->bufferToHtml(mpConsole->showTimeStamps(), y, mPB.x() + 1));
} else { // inside lines of selection
text.append(mpBuffer->bufferToHtml(mShowTimeStamps, y));
text.append(mpBuffer->bufferToHtml(mpConsole->showTimeStamps(), y));
}
}
text.append(qsl(" </div></body>\n"
@ -1650,7 +1626,7 @@ void TTextEdit::slot_copySelectionToClipboardImage()
for (int y = mPA.y(), total = mPB.y() + 1; y < total; ++y) {
const QString lineText{mpBuffer->lineBuffer.at(y)};
// Will accumulate the width in pixels of the current line:
int lineWidth{(mShowTimeStamps ? mTimeStampWidth : 0) * mFontWidth};
auto lineWidth{(mpConsole->showTimeStamps() ? mudlet::smTimeStampFormat.size() : 0) * mFontWidth};
// Accumulated width in "normal" width characters:
int column{};
QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Grapheme, lineText);
@ -1672,10 +1648,10 @@ void TTextEdit::slot_copySelectionToClipboardImage()
// The timestamp is (currently) 13 "normal width" characters
// but that might not always be the case in some future I18n
// situations:
lineWidth = (mShowTimeStamps ? mTimeStampWidth + column : column) * mFontWidth;
lineWidth = (mpConsole->showTimeStamps() ? mudlet::smTimeStampFormat.size() + column : column) * mFontWidth;
indexOfChar = nextBoundary;
}
largestLine = std::max(lineWidth, largestLine);
largestLine = std::max(static_cast<int>(lineWidth), largestLine);
}
auto widthpx = std::min(65500, largestLine);
@ -2011,6 +1987,11 @@ void TTextEdit::resizeEvent(QResizeEvent* event)
}
QWidget::resizeEvent(event);
if (!mIsLowerPane
&& (mpConsole->getType() & (TConsole::MainConsole | TConsole::UserWindow | TConsole::SubConsole))) {
mpConsole->raiseMudletResizeEvent();
}
}
void TTextEdit::wheelEvent(QWheelEvent* e)
@ -2114,7 +2095,7 @@ int TTextEdit::bufferScrollDown(int lines)
}
}
int TTextEdit::getColumnCount()
int TTextEdit::getColumnCount() const
{
int charWidth;
@ -2127,7 +2108,7 @@ int TTextEdit::getColumnCount()
return width() / charWidth;
}
int TTextEdit::getRowCount()
int TTextEdit::getRowCount() const
{
int rowHeight;

View file

@ -91,8 +91,9 @@ public:
void resetHScrollbar() { mScreenOffset = 0; mMaxHRange = 0; }
int getScreenHeight() const { return mScreenHeight; }
void searchSelectionOnline();
int getColumnCount();
int getRowCount();
int getColumnCount() const;
int getRowCount() const;
void toggleTimeStamps(const bool);
#if defined(DEBUG_CODEPOINT_PROBLEMS)
void reportCodepointErrors();
@ -135,10 +136,8 @@ public:
// How many lines the screen scrolled since it was last rendered.
int mScrollVector;
QRegion mSelectedRegion;
bool mShowTimeStamps;
public slots:
void slot_toggleTimeStamps(const bool);
void slot_copySelectionToClipboard();
void slot_selectAll();
void slot_scrollBarMoved(int);
@ -224,10 +223,6 @@ private:
// probably be 1 (so that a tab is just treated as a space), 2, 4 and 8,
// in the past it was typically 8 and this is what we'll use at present:
int mTabStopwidth;
// How many normal width characters that are used for the time stamps; it
// would only be valid to change this by clearing the buffer first - so
// making this a const value for the moment:
const int mTimeStampWidth;
#if defined(DEBUG_CODEPOINT_PROBLEMS)
bool mShowAllCodepointIssues = false;

View file

@ -735,7 +735,7 @@ void cTelnet::checkNAWS()
}
// Use the smaller of the screen width or the wrapAt, then subtract the
// width of the time stamps if they are showing:
int naws_x = std::min(pHost->mScreenWidth, pHost->mWrapAt) - (pHost->mpConsole->mUpperPane->mShowTimeStamps ? TBuffer::csmTimeStampFormat.size() : 0);
int naws_x = std::min(pHost->mScreenWidth, pHost->mWrapAt) - (pHost->mpConsole->showTimeStamps() ? mudlet::smTimeStampFormat.size() : 0);
int naws_y = pHost->mScreenHeight;
if ((naws_y > 0) && (myOptionState[static_cast<size_t>(OPT_NAWS)]) && ((mNaws_x != naws_x) || (mNaws_y != naws_y))) {
sendNAWS(naws_x, naws_y);
@ -2879,6 +2879,12 @@ void cTelnet::setGMCPVariables(const QByteArray& msg)
data = transcodedMsg.section(QChar::LineFeed, 1);
}
if (data.trimmed().isEmpty()) { // Example: Core.Ping
// Pass empty table/object to Lua
mpHost->mLuaInterpreter.setGMCPTable(packageMessage, qsl("{}"));
return;
}
if (transcodedMsg.startsWith(qsl("Client.GUI"), Qt::CaseInsensitive)) {
if (!mpHost->mAcceptServerGUI) {
return;

View file

@ -268,8 +268,10 @@ dlgProfilePreferences::dlgProfilePreferences(QWidget* pParentWidget, Host* pHost
emit signal_resetMainWindowShortcutsToDefaults();
});
mDisplayFont = pHost->getDisplayFont();
pushButton_fontDialog->setText(QString("%1, %2, %3pt").arg(mDisplayFont.family()).arg(mDisplayFont.styleName()).arg(mDisplayFont.pointSize()));
if (pHost) {
mDisplayFont = pHost->getDisplayFont();
pushButton_fontDialog->setText(QString("%1, %2, %3pt").arg(mDisplayFont.family()).arg(mDisplayFont.styleName()).arg(mDisplayFont.pointSize()));
}
connect(pushButton_fontDialog, &QPushButton::clicked, this, [this, pHost](){
bool ok;

View file

@ -321,8 +321,7 @@ dlgTriggerEditor::dlgTriggerEditor(Host* pH)
// option areas
mpErrorConsole = new TConsole(mpHost, qsl("errors_%1").arg(hostName), TConsole::ErrorConsole, this);
mpErrorConsole->setWrapAt(100);
mpErrorConsole->mUpperPane->slot_toggleTimeStamps(true);
mpErrorConsole->mLowerPane->slot_toggleTimeStamps(true);
mpErrorConsole->slot_toggleTimeStamps(true);
mpErrorConsole->print(qsl("%1\n").arg(tr("*** starting new session ***")));
mpErrorConsole->setMinimumHeight(100);
mpErrorConsole->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum);

View file

@ -19,6 +19,7 @@
"ansi2string": "ansi2string(text)",
"appendBuffer": "appendBuffer(name)",
"appendCmdLine": "appendCmdLine([name], text)",
"appendLog": "appendLog(text)",
"appendScript": "appendScript(scriptName, luaCode, [occurrence])",
"auditAreas": "auditAreas()",
"bg": "bg([window, ]colorName)",

View file

@ -169,6 +169,8 @@ function db:_sql_values(values)
elseif t == "table" and v._timestamp ~= nil then
if not v._timestamp then
s = "NULL"
elseif v._timestamp == "CURRENT_TIMESTAMP" then
s = "datetime('now')"
else
s = "datetime('" .. v._timestamp .. "', 'unixepoch')"
end
@ -196,6 +198,7 @@ function db:safe_name(name)
return name
end
function db:_isActiveDBName(db_name)
db_name = db:safe_name(db_name)
@ -206,6 +209,73 @@ function db:_isActiveDBName(db_name)
)
end
local VALIDATION_OPTIONS = {
"ABORT",
"FAIL",
"IGNORE",
"REPLACE",
"ROLLBACK"
}
---@param validations string
---@return boolean is_valid
---@return string msg
function db:_validate_validations(validations)
if type(validations) ~= "string" then
return false, "_validations must be a string. Received "..type(validations)
elseif table.contains(VALIDATION_OPTIONS, validations) then
return true, ""
end
return false, '_validations must be one of: {"ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"}. Received: '..validations
end
---@param unique_constraints string|table
---@return boolean is_valid
---@return string msg
function db:_validate_unique_contraints(unique_constraints)
local is_valid, msg = true, ""
local type_of = type(unique_constraints)
local is_string = type_of == "string"
local is_table = type_of == "table"
if is_string then
-- pass
elseif is_table then
local msgs = {}
for _, unique_constraint in ipairs(unique_constraints) do
type_of = type(unique_constraint)
is_string = type_of == "string"
is_table = type_of == "table"
if is_string then
-- pass
elseif is_table then
for _, value in ipairs(unique_constraint) do
type_of = type(value)
if type_of ~= "string" then
is_valid = false
table.insert(msgs, "Multi-column definitions for _unique must be a list of strings, for example: _unique = { {'foo', 'bar'} }. Received "..type_of..".")
end
end
else
is_valid = false
table.insert(msgs, "Members of _unique must be a string or table. Received ".. type_of..".")
end
end
msg = table.concat(msgs, "\n")
else
is_valid = false
msg = "_unique must be a string or a table. Received "..type_of.."."
end
return is_valid, msg
end
--- Creates and/or modifies an existing database. This function is safe to define at a top-level of a Mudlet
--- script: in fact it is recommended you run this function at a top-level without any kind of guards.
--- If the named database does not exist it will create it. If the database does exist then it will add
@ -259,46 +329,72 @@ function db:create(db_name, sheets, force)
db.__env = luasql.sqlite3()
end
local is_valid, msgs = true, {}
local schema = {}
db_name = db:safe_name(db_name)
-- We need to separate the actual column configuration from the meta-configuration of the desired
-- sheet. {sheet={"column"}} verses {sheet={"column"}, _index={"column"}}. In the former we are
-- creating a database with a single field; in the latter we are also adding an index on that
-- field. The db package reserves any key that begins with an underscore to be special and syntax
-- for its own use.
for sheet_name, sheet in pairs(sheets) do
local columns = {}
local options = {}
-- the sheet was provided in {"column1", "column2"} format
if sheet[1] ~= nil then
-- assume field types are text, and should default to ""
for _, col_name in pairs(sheet) do
columns[col_name] = ""
end
-- sheet provided in {"column1" = default} format
else
for key, value in pairs(sheet) do
if string.starts(key, "_") then
options[key] = value
else
columns[key] = value
end
end
end
if options._violations then
local is_validations_valid, msg = db:_validate_validations(options._violations)
if is_validations_valid == false then
is_valid = false
table.insert(msgs, "db:create - "..sheet_name.." - "..msg)
end
else
options._violations = "FAIL"
end
if options._unique then
local is_unique_valid, msg = db:_validate_unique_contraints(options._unique)
if is_unique_valid == false then
is_valid = false
table.insert(msgs, "db:create - "..sheet_name.." - "..msg)
end
end
schema[sheet_name] = { columns = columns, options = options }
end
assert(is_valid, table.concat(msgs, "\n"))
if not db:_isActiveDBName(db_name) then
db.__conn[db_name] = db.__env:connect(getMudletHomeDir() .. "/Database_" .. db_name .. ".db")
db.__conn[db_name]:setautocommit(false)
db.__autocommit[db_name] = true
end
db.__schema[db_name] = {}
db.__schema[db_name] = schema
-- We need to separate the actual column configuration from the meta-configuration of the desired
-- sheet. {sheet={"column"}} verses {sheet={"column"}, _index={"column"}}. In the former we are
-- creating a database with a single field; in the latter we are also adding an index on that
-- field. The db package reserves any key that begins with an underscore to be special and syntax
-- for its own use.
for s_name, sht in pairs(sheets) do
local options = {}
if sht[1] ~= nil then
-- in case the sheet was provided in the sheet = {"column1", "column2"} format:
local t = {} -- assume field types are text, and should default to ""
for k, v in pairs(sht) do
t[v] = ""
end
sht = t
else -- sheet provided in the sheet = {"column1" = default} format
for k, v in pairs(sht) do
if string.starts(k, "_") then
options[k] = v
sht[k] = nil
end
end
end
if not options._violations then
options._violations = "FAIL"
end
db.__schema[db_name][s_name] = { columns = sht, options = options }
db:_migrate(db_name, s_name, force)
for sheet_name, _ in pairs(sheets) do
db:_migrate(db_name, sheet_name, force)
end
return db:get_database(db_name)
end
@ -483,64 +579,87 @@ function db:_migrate(db_name, s_name, force)
end
function db:_build_create_table_sql(schema, s_name)
local sql_column = ', "%s" %s NULL'
local sql_column = '"%s" %s NULL'
local sql_column_default = sql_column .. ' DEFAULT %s'
local on_conflict = "ON CONFLICT "..schema.options._violations
local sql_chunks = { "CREATE TABLE ", s_name, '("_row_id" INTEGER PRIMARY KEY AUTOINCREMENT' }
local sql_chunks = { '"_row_id" INTEGER PRIMARY KEY AUTOINCREMENT' }
local unique_column_constraints = {}
local unique_table_constraints = {}
-- Validations were already performed in db:create, so the only thing
-- we need to do here is filter our unique constraints to the appropirate
-- tables.
--
-- Into unique_column_constraints when the a column is unique on its own
-- and into unique_table_constraints when columns are grouped together.
if type(schema.options._unique) == "string" then
table.insert(unique_column_constraints, schema.options._unique)
elseif type(schema.options._unique) == "table" then
for _, unique_constraint in ipairs(schema.options._unique) do
if type(unique_constraint) == "string" then
table.insert(unique_column_constraints, unique_constraint)
elseif type(unique_constraint) == "table" then
table.insert(
unique_table_constraints,
'UNIQUE("'..table.concat(unique_constraint, '", "')..'") '..on_conflict
)
end
end
end
-- We iterate over every defined column, and add a line which creates it.
for key, value in pairs(schema.columns) do
for col_name, col_schema in pairs(schema.columns) do
local sql = ""
if value == nil then
sql = sql_column:format(key, db:_sql_type(value))
if col_schema == nil then
sql = sql_column:format(col_name, db:_sql_type(col_schema))
else
sql = sql_column_default:format(key, db:_sql_type(value), db:_sql_convert(value))
sql = sql_column_default:format(col_name, db:_sql_type(col_schema), db:_sql_convert(col_schema))
end
if (type(schema.options._unique) == "table" and table.contains(schema.options._unique, key))
or (type(schema.options._unique) == "string" and schema.options._unique == key) then
sql = sql .. " UNIQUE"
if table.contains(unique_column_constraints, col_name) then
sql = sql .. " UNIQUE "..on_conflict
end
sql_chunks[#sql_chunks + 1] = sql
end
sql_chunks[#sql_chunks + 1] = ")"
-- Add in the unique constraints
for _, unique_table_constraint in ipairs(unique_table_constraints) do
sql_chunks[#sql_chunks + 1] = "UNIQUE("..table.concat(unique_table_constraint, ", ")..")"
end
return table.concat(sql_chunks, "")
return "CREATE TABLE " .. s_name.. " ("..table.concat(sql_chunks, ", ")..")"
end
-- NOT LUADOC
-- Creates any indexes which do not yet exist in the given database.
function db:_migrate_indexes(conn, s_name, schema, current_columns)
local sql_create_index = "CREATE %s IF NOT EXISTS %s ON %s (%s);"
local opt = { _unique = "UNIQUE INDEX", _index = "INDEX" } -- , _check = "CHECK"}
local sql_create_index = "CREATE INDEX IF NOT EXISTS %s ON %s (%s);"
local sql = ""
for option_type, options in pairs(schema.options) do
if option_type == "_unique" or option_type == "_index" then
for _, value in pairs(options) do
-- If an index references a column which does not presently exist within the schema
-- this will fail.
if db:_index_valid(current_columns, value) then
--assert(db:_index_valid(current_columns, value),
-- "In sheet "..s_name.." an index field is specified that does not exist.")
local sql = sql_create_index:format(
opt[option_type], db:_index_name(s_name, value), s_name, db:_sql_columns(value)
)
db:echo_sql(sql)
conn:execute(sql)
end
if (type(schema.options._index) == "table") then
for _, value in pairs(schema.options._index) do
-- If an index references a column which does not presently exist within the schema
-- this will fail.
if db:_index_valid(current_columns, value) then
--assert(db:_index_valid(current_columns, value),
-- "In sheet "..s_name.." an index field is specified that does not exist.")
sql = sql_create_index:format(
db:_index_name(s_name, value),
s_name,
db:_sql_columns(value)
)
db:echo_sql(sql)
conn:execute(sql)
end
end
end
end
--- Adds one or more new rows to the specified sheet. If any of these rows would violate a UNIQUE index,
--- a lua error will be thrown and execution will cancel. As such it is advisable that if you use a UNIQUE
--- index, you test those values before you attempt to insert a new row. <br/><br/>
@ -566,7 +685,7 @@ function db:add(sheet, ...)
assert(s_name, "First argument to db:add must be a proper Sheet object.")
local conn = db.__conn[db_name]
local sql_insert = "INSERT OR %s INTO %s %s VALUES %s"
local sql_insert = "INSERT INTO %s %s VALUES %s"
for _, t in ipairs({ ... }) do
if t._row_id then
@ -574,11 +693,16 @@ function db:add(sheet, ...)
t._row_id = nil
end
local sql = sql_insert:format(db.__schema[db_name][s_name].options._violations, s_name, db:_sql_fields(t), db:_sql_values(t))
local sql = sql_insert:format(
s_name,
db:_sql_fields(t),
db:_sql_values(t)
)
db:echo_sql(sql)
local result, msg = conn:execute(sql)
if not result then
if result == nil then
printError(msg, true, false)
return nil, msg
end
end
@ -611,10 +735,11 @@ function db:fetch_sql(sheet, sql)
-- if we had a syntax error in our SQL, cur will be nil
if cur and cur ~= 0 then
local results = {}
local columns = cur:getcolnames()
local row = cur:fetch({}, "a")
while row do
results[#results + 1] = db:_coerce_sheet(sheet, row)
results[#results + 1] = db:_coerce_sheet(columns, sheet, row)
row = cur:fetch({}, "a")
end
cur:close()
@ -748,11 +873,9 @@ function db:aggregate(field, fn, query, distinct)
return count
end
-- Only datetime left
-- the value, count, is currently in a UTC timestamp
local localtime = datetime:parse(count, nil, true)
-- convert it into a UTC timestamp as datetime:parse parses it in the local time context
count = db:Timestamp(localtime + datetime:calculate_UTCdiff(localtime))
return count
local utc_epoch = datetime:parse(count, nil, true)
local locale_diff = datetime:calculate_UTCdiff(utc_epoch)
return db:Timestamp(utc_epoch + locale_diff)
else
return 0
end
@ -940,7 +1063,7 @@ function db:update(sheet, tbl)
local conn = db.__conn[db_name]
local sql_chunks = { "UPDATE OR", db.__schema[db_name][s_name].options._violations, s_name, "SET" }
local sql_chunks = { "UPDATE", s_name, "SET" }
local set_chunks = {}
local set_block = [["%s" = %s]]
@ -1013,12 +1136,17 @@ function db:set(field, value, query)
local conn = db.__conn[db_name]
local sql_update = [[UPDATE OR %s %s SET "%s" = %s]]
local sql_update = [[UPDATE %s SET "%s" = %s]]
if query then
sql_update = sql_update .. [[ WHERE %s]]
end
local sql = sql_update:format(db.__schema[db_name][s_name].options._violations, s_name, field.name, db:_coerce(field, value), query)
local sql = sql_update:format(
s_name,
field.name,
db:_coerce(field, value),
query
)
db:echo_sql(sql)
assert(conn:execute(sql))
@ -1048,20 +1176,24 @@ end
-- After a table so retrieved from the database, this function coerces values to
-- their proper types. Specifically, numbers and datetimes become the proper
-- types.
function db:_coerce_sheet(sheet, tbl)
function db:_coerce_sheet(columns, sheet, tbl)
if tbl then
tbl._row_id = tonumber(tbl._row_id)
for k, v in pairs(tbl) do
for _, k in pairs(columns) do
if k ~= "_row_id" then
local field = sheet[k]
if field.type == "number" then
tbl[k] = tonumber(tbl[k]) or tbl[k]
elseif field.type == "datetime" then
-- the value, tbl[k], is currently in a UTC timestamp
local localtime = datetime:parse(tbl[k], nil, true)
-- convert it into a UTC timestamp as datetime:parse parses it in the local time context
tbl[k] = db:Timestamp(localtime + datetime:calculate_UTCdiff(localtime))
if (tbl[k] == nil) then
tbl[k] = db:Timestamp(nil)
else
-- the value, tbl[k], is a UTC timestamp
local utc_epoch = datetime:parse(tbl[k], nil, true)
local locale_diff = datetime:calculate_UTCdiff(utc_epoch)
tbl[k] = db:Timestamp(utc_epoch + locale_diff)
end
end
end
end
@ -1079,10 +1211,12 @@ function db:_coerce(field, value)
if type(value) == "table" and value._isNull then
return "NULL"
elseif field.type == "number" then
return tonumber(value) or "'" .. value .. "'"
return tonumber(value) or ("'" .. value .. "'")
elseif field.type == "datetime" then
if value._timestamp == false then
return "NULL"
elseif value._timestamp == "CURRENT_TIMESTAMP" then
return "datetime('now')"
else
return "datetime('" .. value._timestamp .. "', 'unixepoch')" or "'" .. value .. "'"
end
@ -1460,17 +1594,22 @@ end
--- <b><u>TODO</u></b>
function db:Timestamp(ts, fmt)
local dt = {}
if type(ts) == "table" then
dt._timestamp = os.time(ts)
elseif type(ts) == "number" then
dt._timestamp = ts
elseif type(ts) == "string" and
assert(ts == "CURRENT_TIMESTAMP", "The only strings supported by db.DateTime:new is CURRENT_TIMESTAMP") then
dt._timestamp = "CURRENT_TIMESTAMP"
elseif ts == nil then
dt._timestamp = false
if ts == nil then
dt._timestamp = false
elseif ts == "CURRENT_TIMESTAMP" then
dt._timestamp = "CURRENT_TIMESTAMP"
else
assert(nil, "Invalid value passed to db.Timestamp()")
local t = type(ts)
if t == "table" then
dt._timestamp = os.time(ts)
elseif t == "number" then
dt._timestamp = ts
elseif t == "string" then
dt._timestamp = datetime:parse(ts, fmt, true)
else
error("Invalid value passed to db.Timestamp()")
end
end
return setmetatable(dt, db.__TimestampMT)
end
@ -1506,7 +1645,7 @@ db.__SheetMT = {
local rt
if assert(field, errormsg:format(k, sht_name, db_name)) then
field_type = type(field)
if field_type == "table" and field._timestamp then
if field_type == "table" and field._timestamp ~= nil then
field_type = "datetime"
end

View file

@ -52,7 +52,7 @@ function datetime:calculate_UTCdiff(ts)
local date, time = os.date, os.time
local utc = date('!*t', ts)
local lcl = date('*t', ts)
lcl.isdst = os.date("*t")["isdst"]
lcl.isdst = false
return os.difftime(time(lcl), time(utc))
end
@ -140,7 +140,7 @@ function datetime:parse(source, format, as_epoch)
dt.min = tonumber(m.minute)
dt.sec = tonumber(m.second)
dt.isdst = os.date("*t")["isdst"]
dt.isdst = os.date("*t", os.time(dt))["isdst"]
if as_epoch then
return os.time(dt)

View file

@ -212,7 +212,7 @@ describe("Tests DB.lua functions", function()
}
mydb = db:create("mydbttestingonly", { sheet = newschema })
assert.are.same(db.__schema.mydbttestingonly.sheet.columns, newschema)
assert.are.same(db.__schema.mydbttestingonly.sheet.columns, {row1 = "", row2 = 0, row3 = 0})
end)
it("Should add a column of type string successfully to an empty db", function()
@ -226,13 +226,13 @@ describe("Tests DB.lua functions", function()
}
mydb = db:create("mydbttestingonly", { sheet = newschema })
assert.are.same(db.__schema.mydbttestingonly.sheet.columns, newschema)
assert.are.same(db.__schema.mydbttestingonly.sheet.columns, {row1 = "", row2 = 0, row3 = ""})
end)
it("Should add a column successfully to a filled db", function()
db:add(mydb.sheet, {row1 = "some data"})
local newschema = {
local sheet = {
row1 = "",
row2 = 0,
row3 = "",
@ -241,8 +241,7 @@ describe("Tests DB.lua functions", function()
_violations = "REPLACE"
}
mydb = db:create("mydbttestingonly", { sheet = newschema })
assert.are.same(db.__schema.mydbttestingonly.sheet.columns, newschema)
mydb = db:create("mydbttestingonly", { sheet = sheet })
local newrow = db:fetch(mydb.sheet)[1]
assert.are.same("some data", newrow.row1)
assert.are.same("", newrow.row3)
@ -303,7 +302,7 @@ describe("Tests DB.lua functions", function()
cur:close()
end
assert.equals(3, #results)
assert.equals(2, #results)
for _, v in ipairs(results) do
@ -321,12 +320,6 @@ describe("Tests DB.lua functions", function()
sql = 'CREATE INDEX idx_sheet_c_name ' ..
'ON sheet ("name")'
}
elseif v.name == "idx_sheet_c_id" then
expected = { type = "index", name = "idx_sheet_c_id",
tbl_name = "sheet",
sql = 'CREATE UNIQUE INDEX idx_sheet_c_id ' ..
'ON sheet ("id")'
}
end
assert.are.same(expected, v)
@ -1033,4 +1026,99 @@ describe("Tests DB.lua functions", function()
assert.are.same(results, test)
end)
end)
describe("Tests, if timestamp handling works as intended",
function()
local input = {
current = db:Timestamp("CURRENT_TIMESTAMP"),
niled = db:Timestamp(nil),
epoched = db:Timestamp(1748288082), -- 2025-05-26T19:34:42+00:00
tabled = db:Timestamp({year=1970, month=1, day=1, hour=10, sec=1})
}
before_each(function()
mydb = db:create("mydbttimestamptesting", { sheet = input })
end)
after_each(function()
db:close()
local filename = getMudletHomeDir() .. "/Database_mydbttimestamptesting.db"
os.remove(filename)
mydb = nil
end)
it("should fetch a timestamp for CURRENT_TIMESTAMP.",
function()
db:add(mydb.sheet, input)
local results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local result = results[1]
assert.is_true(result.current._timestamp ~= nil)
end)
it("should fetch the same epoch timestamp as what was put in.",
function()
db:add(mydb.sheet, input)
local results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local result = results[1]
assert.are.same(result.epoched:as_number(), input.epoched:as_number())
assert.are.same(result.epoched:as_string(), input.epoched:as_string())
assert.are.same(result.epoched:as_table(), input.epoched:as_table())
end)
it("should fetch the same table timestamp as what was put in.",
function()
db:add(mydb.sheet, input)
local results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local result = results[1]
assert.are.same(result.tabled:as_number(), input.tabled:as_number())
assert.are.same(result.tabled:as_string(), input.tabled:as_string())
assert.are.same(result.tabled:as_table(), input.tabled:as_table())
end)
it("should fetch the same niled timestamp as what was put in.",
function()
db:add(mydb.sheet, input)
local results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local result = results[1]
assert.are.same(result.niled._timestamp, input.niled._timestamp)
end)
it("should update without changing a timestamp's value.",
function()
db:add(mydb.sheet, input)
local results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local first_result = results[1]
db:update(mydb.sheet, results[1])
results = db:fetch(mydb.sheet)
assert.is_true(#results == 1)
local second_result = results[1]
assert.are.same(first_result.current:as_number(), second_result.current:as_number())
assert.are.same(first_result.current:as_string(), second_result.current:as_string())
assert.are.same(first_result.current:as_table(), second_result.current:as_table())
assert.are.same(first_result.epoched:as_number(), second_result.epoched:as_number())
assert.are.same(first_result.epoched:as_string(), second_result.epoched:as_string())
assert.are.same(first_result.epoched:as_table(), second_result.epoched:as_table())
assert.are.same(first_result.tabled:as_number(), second_result.tabled:as_number())
assert.are.same(first_result.tabled:as_string(), second_result.tabled:as_string())
assert.are.same(first_result.tabled:as_table(), second_result.tabled:as_table())
assert.are.same(first_result.niled._timestamp, second_result.niled._timestamp)
end)
end)
end)

View file

@ -0,0 +1,41 @@
describe("Tests C++ functions in the Miscallaneous category", function()
describe("Tests the functionality of getOS", function()
it("should return the correct number of values for the current OS", function()
local results = {getOS()}
-- Linux returns 4 values, all others return 3
if results[1] == "linux" then
assert.equals(4, #results)
else
assert.equals(3, #results)
end
end)
it("should return string values", function()
local osName, osVersion, osType = getOS()
assert.is_string(osName)
assert.is_string(osVersion)
if osType then
assert.is_string(osType)
end
end)
it("should return a valid OS name as first value", function()
local validOSNames = {
"windows", "mac", "linux", "cygwin", "hurd",
"freebsd", "kfreebsd", "openbsd", "netbsd",
"bsd4", "unix", "unknown"
}
local osName = getOS()
assert.is_true(table.contains(validOSNames, osName))
end)
it("should not return empty strings", function()
local osName, osVersion, osType = getOS()
assert.is_true(osName ~= "")
assert.is_true(osVersion ~= "")
if osType then
assert.is_true(osType ~= "")
end
end)
end)
end)

View file

@ -712,8 +712,8 @@ void mudlet::init()
// load bundled fonts
mFontManager.addFonts();
// Initialise a couple of QMaps with elements that must be translated into
// the current GUI Language
// Initialise a couple of QMaps and some other elements that must be
// translated into the current GUI Language
loadMaps();
setupTrayIcon();
@ -1245,6 +1245,18 @@ void mudlet::loadMaps()
{"WINDOWS-1257", tr("WINDOWS-1257 (Baltic)")},
//: Keep the English translation intact, so if a user accidentally changes to a language they don't understand, they can change back e.g. ISO 8859-2 (Центральная Европа/Central European)
{"WINDOWS-1258", tr("WINDOWS-1258 (Vietnamese)")}};
/*: This represents the format of the timestamps shown alongside the texts
* in a console and might require translation for a few locales; the content
* is as per QDateTime::toString(...) and needs to follow the rules for that
* function as well as being suitable for the translation locale.
*/
smTimeStampFormat = tr("hh:mm:ss.zzz ");
/*: This represents the format of the timestamps shown for lines that do not
* have a timestamp in a console that is showing them. If localised this
* should be set to the same format and length as the smTimeStampFormat:
*/
smBlankTimeStamp = tr("------------ ");
}
// migrates the Central Debug Console to the next available host, if any

View file

@ -208,6 +208,11 @@ public:
inline static bool smMirrorToStdOut = false;
// adjust Mudlet settings to match Steam's requirements
inline static bool smSteamMode = false;
// This may need to be localised, it represents the format of the timestamp
inline static QString smTimeStampFormat = qsl("hh:mm:ss.zzz ");
// If localised this should be set to the same format and length as the
// smTimeStampFormat:
inline static QString smBlankTimeStamp = qsl("------------ ");
void showEvent(QShowEvent*) override;

File diff suppressed because it is too large Load diff