mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
Merge branch 'development' into wrap-overhaul
This commit is contained in:
commit
e6c516e3ee
23 changed files with 1918 additions and 1356 deletions
14
src/Host.cpp
14
src/Host.cpp
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,6 +433,8 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void sendCmdLine(const QString& cmd);
|
||||
|
||||
cTelnet mTelnet;
|
||||
QPointer<TMainConsole> mpConsole;
|
||||
QPointer<dlgPackageManager> mpPackageManager;
|
||||
|
|
|
|||
107
src/TBuffer.cpp
107
src/TBuffer.cpp
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1553,3 +1553,4 @@ void TCommandLine::slot_saveHistory()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
41
src/mudlet-lua/tests/Miscallaneous_spec.lua
Normal file
41
src/mudlet-lua/tests/Miscallaneous_spec.lua
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue