From e6eaa3f4fa6de21b6e097da1314940c01dfb8ac9 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Sat, 3 Jan 2026 15:56:36 +0100
Subject: WIP: LLM-assisted xTq
---
xT/xTq.cpp | 1224 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
xT/xTq.h | 216 ++++++++++-
xT/xTq.qml | 413 ++++++++++++++++++--
3 files changed, 1814 insertions(+), 39 deletions(-)
diff --git a/xT/xTq.cpp b/xT/xTq.cpp
index a6d48bf..2ad37bc 100644
--- a/xT/xTq.cpp
+++ b/xT/xTq.cpp
@@ -17,15 +17,1231 @@
*/
#include "xC-proto.cpp"
+#include "xTq.h"
-#include
-
+#include
+#include
#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
-#include "xTq.h"
+// --- IRC formatting utilities ------------------------------------------------
+
+static QColor
+convert_color(int16_t color)
+{
+ static const uint16_t base16[] = {
+ 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
+ 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
+ };
+ if (color < 16) {
+ uint8_t r = 0xf & (base16[color] >> 8);
+ uint8_t g = 0xf & (base16[color] >> 4);
+ uint8_t b = 0xf & (base16[color]);
+ return QColor(r * 0x11, g * 0x11, b * 0x11);
+ }
+ if (color >= 216) {
+ uint8_t g = 8 + (color - 216) * 10;
+ return QColor(g, g, g);
+ }
+
+ uint8_t i = color - 16, r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6;
+ return QColor(
+ !r ? 0 : 55 + 40 * r,
+ !g ? 0 : 55 + 40 * g,
+ !b ? 0 : 55 + 40 * b);
+}
+
+static void
+convert_item_formatting(
+ Relay::ItemData *item, QTextCharFormat &cf, bool &inverse)
+{
+ if (dynamic_cast(item)) {
+ cf = QTextCharFormat();
+ inverse = false;
+ } else if (dynamic_cast(item)) {
+ if (cf.fontWeight() <= QFont::Normal)
+ cf.setFontWeight(QFont::Bold);
+ else
+ cf.setFontWeight(QFont::Normal);
+ } else if (dynamic_cast(item)) {
+ cf.setFontItalic(!cf.fontItalic());
+ } else if (dynamic_cast(item)) {
+ cf.setFontUnderline(!cf.fontUnderline());
+ } else if (dynamic_cast(item)) {
+ cf.setFontStrikeOut(!cf.fontStrikeOut());
+ } else if (dynamic_cast(item)) {
+ inverse = !inverse;
+ } else if (dynamic_cast(item)) {
+ auto families = cf.fontFamilies().toStringList();
+ if (!families.isEmpty() && families.first() == "monospace")
+ cf.setFontFamilies(QStringList());
+ else
+ cf.setFontFamilies(QStringList() << "monospace");
+ } else if (auto data = dynamic_cast(item)) {
+ if (data->color < 0) {
+ cf.clearForeground();
+ } else {
+ cf.setForeground(convert_color(data->color));
+ }
+ } else if (auto data = dynamic_cast(item)) {
+ if (data->color < 0) {
+ cf.clearBackground();
+ } else {
+ cf.setBackground(convert_color(data->color));
+ }
+ }
+}
+
+static void
+convert_links(const QTextCharFormat &format, const QString &text,
+ std::vector &result)
+{
+ static QRegularExpression link_re(
+ R"(https?://)"
+ R"((?:[^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+)"
+ R"((?:[^\[\](){}<>"'\s,.:]|\([^\[\](){}<>"'\s]*\)))");
+
+ qsizetype end = 0;
+ for (const QRegularExpressionMatch &m : link_re.globalMatch(text)) {
+ if (end < m.capturedStart()) {
+ result.emplace_back(BufferLineItem{
+ format, text.sliced(end, m.capturedStart() - end)});
+ }
+
+ BufferLineItem item{format, m.captured()};
+ item.format.setAnchor(true);
+ item.format.setAnchorHref(m.captured());
+ result.emplace_back(std::move(item));
+
+ end = m.capturedEnd();
+ }
+ if (!end)
+ result.emplace_back(BufferLineItem{format, text});
+ else if (end < text.length())
+ result.emplace_back(BufferLineItem{format, text.sliced(end)});
+}
+
+static std::vector
+convert_items(const std::vector> &items)
+{
+ QTextCharFormat cf;
+ std::vector result;
+ bool inverse = false;
+ for (const auto &it : items) {
+ auto text = dynamic_cast(it.get());
+ if (!text) {
+ convert_item_formatting(it.get(), cf, inverse);
+ continue;
+ }
+
+ auto item_format = cf;
+ auto item_text = QString::fromStdWString(text->text);
+ if (inverse) {
+ auto fg = item_format.foreground();
+ auto bg = item_format.background();
+ item_format.setBackground(fg);
+ item_format.setForeground(bg);
+ }
+ convert_links(item_format, item_text, result);
+ }
+ return result;
+}
+
+static QString
+irc_to_rich_text(const QString &irc)
+{
+ QTextDocument doc;
+ QTextCursor cursor(&doc);
+ QTextCharFormat cf;
+ bool bold = false, monospace = false, italic = false, crossed = false,
+ underline = false;
+
+ QString current;
+ auto apply = [&]() {
+ if (!current.isEmpty()) {
+ cursor.insertText(current, cf);
+ current.clear();
+ }
+ };
+
+ for (int i = 0; i < irc.length(); ++i) {
+ switch (irc[i].unicode()) {
+ case '\x02':
+ apply();
+ bold = !bold;
+ cf.setFontWeight(bold ? QFont::Bold : QFont::Normal);
+ break;
+ case '\x03':
+ // TODO(p): Decode colours, see xC.
+ break;
+ case '\x11':
+ apply();
+ monospace = !monospace;
+ if (monospace)
+ cf.setFontFamilies(QStringList() << "monospace");
+ else
+ cf.setFontFamilies(QStringList());
+ break;
+ case '\x1d':
+ apply();
+ cf.setFontItalic((italic = !italic));
+ break;
+ case '\x1e':
+ apply();
+ cf.setFontStrikeOut((crossed = !crossed));
+ break;
+ case '\x1f':
+ apply();
+ cf.setFontUnderline((underline = !underline));
+ break;
+ case '\x0f':
+ apply();
+ bold = monospace = italic = crossed = underline = false;
+ cf = QTextCharFormat();
+ break;
+ default:
+ current += irc[i];
+ }
+ }
+ apply();
+ return doc.toHtml();
+}
+
+static QString
+rich_text_to_irc(const QString &html)
+{
+ QTextDocument doc;
+ doc.setHtml(html);
+
+ QString irc;
+ for (auto block = doc.begin(); block.isValid(); block = block.next()) {
+ for (auto it = block.begin(); it != block.end(); ++it) {
+ auto fragment = it.fragment();
+ if (!fragment.isValid())
+ continue;
+
+ // TODO(p): Colours.
+ QString toggles;
+ auto format = fragment.charFormat();
+ if (format.fontWeight() >= QFont::Bold)
+ toggles += "\x02";
+ if (format.fontFixedPitch())
+ toggles += "\x11";
+ if (format.fontItalic())
+ toggles += "\x1d";
+ if (format.fontStrikeOut())
+ toggles += "\x1e";
+ if (format.fontUnderline())
+ toggles += "\x1f";
+ irc += toggles + fragment.text() + toggles;
+ }
+ if (block.next().isValid())
+ irc += "\n";
+ }
+ return irc;
+}
+
+// --- BufferListModel ---------------------------------------------------------
+
+BufferListModel::BufferListModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+int
+BufferListModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid() || !buffers_)
+ return 0;
+ return buffers_->size();
+}
+
+QVariant
+BufferListModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || !buffers_ ||
+ index.row() < 0 || index.row() >= int(buffers_->size()))
+ return {};
+
+ const auto &buffer = buffers_->at(index.row());
+ switch (role) {
+ case BufferNameRole:
+ return buffer.buffer_name;
+ case BufferKindRole:
+ return int(buffer.kind);
+ case ServerNameRole:
+ return buffer.server_name;
+ case NewMessagesRole:
+ return buffer.new_messages;
+ case HighlightedRole:
+ return buffer.highlighted;
+ case DisplayTextRole:
+ {
+ auto text = buffer.buffer_name;
+ if (buffer.buffer_name != current && buffer.new_messages)
+ text += " (" + QString::number(buffer.new_messages) + ")";
+ return text;
+ }
+ case IsBoldRole:
+ return buffer.buffer_name != current && buffer.new_messages > 0;
+ case HighlightColorRole:
+ return buffer.highlighted ? QColor(0xff, 0x5f, 0x00) : QColor();
+ }
+ return {};
+}
+
+QHash
+BufferListModel::roleNames() const
+{
+ return {
+ {BufferNameRole, "bufferName"},
+ {BufferKindRole, "bufferKind"},
+ {ServerNameRole, "serverName"},
+ {NewMessagesRole, "newMessages"},
+ {HighlightedRole, "highlighted"},
+ {DisplayTextRole, "displayText"},
+ {IsBoldRole, "isBold"},
+ {HighlightColorRole, "highlightColor"}
+ };
+}
+
+void
+BufferListModel::setBuffers(const std::vector *buffers)
+{
+ beginResetModel();
+ buffers_ = buffers;
+ endResetModel();
+}
+
+void
+BufferListModel::setCurrentBuffer(const QString ¤t_)
+{
+ current = current_;
+ emit bufferActivated(current_);
+}
+
+int
+BufferListModel::getCurrentBufferIndex() const
+{
+ if (!buffers_)
+ return -1;
+
+ for (size_t i = 0; i < buffers_->size(); i++) {
+ if (buffers_->at(i).buffer_name == current)
+ return i;
+ }
+ return -1;
+}
+
+void
+BufferListModel::refresh()
+{
+ if (!buffers_)
+ return;
+ dataChanged(index(0), index(buffers_->size() - 1));
+}
+
+void
+BufferListModel::refreshBuffer(int idx)
+{
+ if (!buffers_ || idx < 0 || idx >= int(buffers_->size()))
+ return;
+ auto i = index(idx);
+ dataChanged(i, i);
+}
+
+void
+BufferListModel::bufferAdded(int idx)
+{
+ if (!buffers_)
+ return;
+ beginInsertRows(QModelIndex(), idx, idx);
+ endInsertRows();
+}
+
+void
+BufferListModel::bufferRemoved(int idx)
+{
+ if (!buffers_)
+ return;
+ beginRemoveRows(QModelIndex(), idx, idx);
+ endRemoveRows();
+}
+
+// --- RelayConnection ---------------------------------------------------------
+
+RelayConnection::RelayConnection(QObject *parent)
+ : QObject(parent),
+ socket(new QTcpSocket(this)),
+ date_change_timer(new QTimer(this))
+{
+ buffer_list_model.setBuffers(&buffers);
+
+ connect(socket, &QTcpSocket::connected,
+ this, &RelayConnection::onSocketConnected);
+ connect(socket, &QTcpSocket::disconnected,
+ this, &RelayConnection::onSocketDisconnected);
+ connect(socket, &QTcpSocket::readyRead,
+ this, &RelayConnection::onSocketReadyRead);
+ connect(socket, &QTcpSocket::errorOccurred,
+ this, &RelayConnection::onSocketError);
+
+ connect(date_change_timer, &QTimer::timeout, [this]() {
+ // Update buffer display to show new date marker
+ emit bufferTextChanged();
+
+ // Schedule next update at midnight
+ auto current = QDateTime::currentDateTime();
+ QDateTime midnight(current.date().addDays(1), {});
+ if (midnight > current)
+ date_change_timer->start(current.msecsTo(midnight) + 1);
+ });
+}
+
+void
+RelayConnection::setHost(const QString &host)
+{
+ if (host_ != host) {
+ host_ = host;
+ emit hostChanged();
+ }
+}
+
+void
+RelayConnection::setPort(const QString &port)
+{
+ if (port_ != port) {
+ port_ = port;
+ emit portChanged();
+ }
+}
+
+bool
+RelayConnection::isConnected() const
+{
+ return socket->state() == QAbstractSocket::ConnectedState;
+}
+
+QString
+RelayConnection::prompt() const
+{
+ auto b = const_cast(this)->bufferByName(buffer_current);
+ if (!b) {
+ return "Synchronizing...";
+ } else if (auto it = servers.find(b->server_name); it != servers.end()) {
+ QString prompt = it->second.user;
+ if (!it->second.user_modes.isEmpty())
+ prompt += "(" + it->second.user_modes + ")";
+ if (prompt.isEmpty()) {
+ switch (it->second.state) {
+ case Relay::ServerState::DISCONNECTED: return "disconnected";
+ case Relay::ServerState::CONNECTING: return "connecting";
+ case Relay::ServerState::CONNECTED: return "connected";
+ case Relay::ServerState::REGISTERED: return "registered";
+ case Relay::ServerState::DISCONNECTING: return "disconnecting";
+ }
+ }
+ return prompt;
+ }
+ return {};
+}
+
+QString
+RelayConnection::status() const
+{
+ QString status = buffer_current;
+ if (auto b = const_cast(this)->bufferByName(buffer_current)) {
+ if (!b->modes.isEmpty())
+ status += "(+" + b->modes + ")";
+ if (b->hide_unimportant)
+ status += "";
+ }
+ return status;
+}
+
+QString
+RelayConnection::topic() const
+{
+ auto b = const_cast(this)->bufferByName(buffer_current);
+ if (!b)
+ return {};
+
+ QTextDocument doc;
+ QTextCursor cursor(&doc);
+ for (const auto &it : b->topic) {
+ cursor.setCharFormat(it.format);
+ cursor.insertText(it.text);
+ }
+ return doc.toHtml();
+}
+
+void
+RelayConnection::connectToRelay()
+{
+ if (host_.isEmpty() || port_.isEmpty()) {
+ emit errorOccurred("Host or port not set");
+ return;
+ }
+ socket->connectToHost(host_, port_.toUShort());
+}
+
+void
+RelayConnection::disconnectFromRelay()
+{
+ socket->disconnectFromHost();
+}
+
+void
+RelayConnection::activateBuffer(const QString &name)
+{
+ auto activate = new Relay::CommandData_BufferActivate();
+ activate->buffer_name = name.toStdWString();
+ relaySend(activate);
+}
+
+void
+RelayConnection::activatePreviousBuffer()
+{
+ if (buffers.empty())
+ return;
+
+ int current_index = -1;
+ for (size_t i = 0; i < buffers.size(); i++) {
+ if (buffers[i].buffer_name == buffer_current) {
+ current_index = i;
+ break;
+ }
+ }
+
+ if (current_index > 0) {
+ activateBuffer(buffers[current_index - 1].buffer_name);
+ }
+}
+
+void
+RelayConnection::activateNextBuffer()
+{
+ if (buffers.empty())
+ return;
+
+ int current_index = -1;
+ for (size_t i = 0; i < buffers.size(); i++) {
+ if (buffers[i].buffer_name == buffer_current) {
+ current_index = i;
+ break;
+ }
+ }
+
+ if (current_index >= 0 && (size_t)current_index < buffers.size() - 1) {
+ activateBuffer(buffers[current_index + 1].buffer_name);
+ }
+}
+
+void
+RelayConnection::toggleUnimportant()
+{
+ if (buffer_current.isEmpty())
+ return;
+
+ auto toggle = new Relay::CommandData_BufferToggleUnimportant();
+ toggle->buffer_name = buffer_current.toStdWString();
+ relaySend(toggle);
+}
+
+void
+RelayConnection::sendInput(const QString &input)
+{
+ auto b = bufferByName(buffer_current);
+ if (!b)
+ return;
+
+ auto cmd = new Relay::CommandData_BufferInput();
+ cmd->buffer_name = b->buffer_name.toStdWString();
+ cmd->text = rich_text_to_irc(input).toStdWString();
+
+ b->history.push_back(input);
+ b->history_at = b->history.size();
+
+ relaySend(cmd);
+}
+
+void
+RelayConnection::sendCommand(const QString &command)
+{
+ // For now, just send as input
+ sendInput(command);
+}
+
+void
+RelayConnection::populateBufferDocument(QQuickTextDocument *quickDoc)
+{
+ if (!quickDoc)
+ return;
+
+ QTextDocument *doc = quickDoc->textDocument();
+ if (!doc)
+ return;
+
+ auto b = bufferByName(buffer_current);
+ doc->clear();
+ if (!b)
+ return;
+
+ QTextCursor cursor(doc);
+ for (const auto &line : b->lines) {
+ if (line.is_unimportant && b->hide_unimportant)
+ continue;
+
+ // Timestamp
+ auto dt = QDateTime::fromMSecsSinceEpoch(line.when);
+ auto timestamp = dt.toString("HH:mm:ss ");
+ QTextCharFormat tsFormat;
+ tsFormat.setForeground(QColor(0xbb, 0xbb, 0xbb));
+ tsFormat.setBackground(QColor(0xf8, 0xf8, 0xf8));
+ cursor.insertText(timestamp, tsFormat);
+
+ // Rendition prefix
+ QString prefix;
+ QTextCharFormat pcf;
+ pcf.setFontFamilies(QStringList() << "monospace");
+ pcf.setFontWeight(QFont::Bold);
+ switch (line.rendition) {
+ break; case Relay::Rendition::BARE:
+ break; case Relay::Rendition::INDENT:
+ prefix = " ";
+ break; case Relay::Rendition::STATUS:
+ prefix = " - ";
+ break; case Relay::Rendition::ERROR:
+ prefix = "=!= ";
+ pcf.setForeground(QColor(0xff, 0, 0));
+ break; case Relay::Rendition::JOIN:
+ prefix = "——> ";
+ pcf.setForeground(QColor(0, 0x88, 0));
+ break; case Relay::Rendition::PART:
+ prefix = "<—— ";
+ pcf.setForeground(QColor(0x88, 0, 0));
+ break; case Relay::Rendition::ACTION:
+ prefix = " * ";
+ pcf.setForeground(QColor(0x88, 0, 0));
+ }
+
+ if (!prefix.isEmpty())
+ cursor.insertText(prefix, pcf);
+
+ // Message items
+ for (const auto &it : line.items) {
+ cursor.insertText(it.text, it.format);
+ }
+
+ // Reset format before inserting newline to prevent color spillage
+ cursor.setCharFormat(QTextCharFormat());
+ cursor.insertText("\n");
+ }
+}
+
+QString
+RelayConnection::getInputHistoryUp()
+{
+ auto b = bufferByName(buffer_current);
+ if (!b || b->history_at < 1)
+ return {};
+
+ if (b->history_at == b->history.size())
+ b->input = ""; // Current input is being navigated away from
+ b->history_at--;
+ return b->history.at(b->history_at);
+}
+
+QString
+RelayConnection::getInputHistoryDown()
+{
+ auto b = bufferByName(buffer_current);
+ if (!b || b->history_at >= b->history.size())
+ return {};
+
+ b->history_at++;
+ if (b->history_at == b->history.size())
+ return b->input;
+ return b->history.at(b->history_at);
+}
+
+void
+RelayConnection::requestCompletion(const QString &text, int position)
+{
+ auto b = bufferByName(buffer_current);
+ if (!b)
+ return;
+
+ auto utf8 = text.left(position).toUtf8();
+ auto complete = new Relay::CommandData_BufferComplete();
+ complete->buffer_name = b->buffer_name.toStdWString();
+ complete->text = text.toStdWString();
+ complete->position = utf8.size();
+
+ relaySend(complete, [this, text, position](auto error, auto response) {
+ if (!response) {
+ emit errorOccurred(QString::fromStdWString(error));
+ return;
+ }
+
+ auto result = dynamic_cast(response);
+ if (!result || result->completions.empty())
+ return;
+
+ auto insert = result->completions.at(0);
+ if (result->completions.size() == 1)
+ insert += L" ";
+
+ auto preceding = text.left(result->start);
+ auto completion = preceding + QString::fromStdWString(insert);
+ emit completionResult(completion);
+
+ if (result->completions.size() != 1)
+ emit beepRequested();
+ });
+}
+
+void
+RelayConnection::saveBufferInput(const QString &bufferName, const QString &input, int start, int end)
+{
+ auto b = bufferByName(bufferName);
+ if (!b)
+ return;
+
+ b->input = input;
+ b->input_start = start;
+ b->input_end = end;
+}
+
+QString
+RelayConnection::getBufferInput()
+{
+ auto b = bufferByName(buffer_current);
+ return b ? b->input : QString();
+}
+
+int
+RelayConnection::getBufferInputStart()
+{
+ auto b = bufferByName(buffer_current);
+ return b ? b->input_start : 0;
+}
+
+int
+RelayConnection::getBufferInputEnd()
+{
+ auto b = bufferByName(buffer_current);
+ return b ? b->input_end : 0;
+}
+
+void
+RelayConnection::activateLastBuffer()
+{
+ if (!buffer_last.isEmpty())
+ activateBuffer(buffer_last);
+}
+
+void
+RelayConnection::activateNextHighlighted()
+{
+ int startIdx = -1;
+ for (size_t i = 0; i < buffers.size(); i++) {
+ if (buffers[i].buffer_name == buffer_current) {
+ startIdx = i;
+ break;
+ }
+ }
+
+ if (startIdx < 0)
+ return;
+
+ for (size_t i = 1; i < buffers.size(); i++) {
+ size_t idx = (startIdx + i) % buffers.size();
+ if (buffers[idx].highlighted) {
+ activateBuffer(buffers[idx].buffer_name);
+ return;
+ }
+ }
+}
+
+void
+RelayConnection::activateNextWithActivity()
+{
+ int startIdx = -1;
+ for (size_t i = 0; i < buffers.size(); i++) {
+ if (buffers[i].buffer_name == buffer_current) {
+ startIdx = i;
+ break;
+ }
+ }
+
+ if (startIdx < 0)
+ return;
+
+ for (size_t i = 1; i < buffers.size(); i++) {
+ size_t idx = (startIdx + i) % buffers.size();
+ if (buffers[idx].new_messages > 0) {
+ activateBuffer(buffers[idx].buffer_name);
+ return;
+ }
+ }
+}
+
+void
+RelayConnection::toggleBufferLog()
+{
+ auto log = new Relay::CommandData_BufferLog();
+ log->buffer_name = buffer_current.toStdWString();
+ relaySend(log, [this, name = buffer_current](auto error, auto response) {
+ if (!response) {
+ emit errorOccurred(QString::fromStdWString(error));
+ return;
+ }
+ if (buffer_current != name)
+ return;
+
+ auto result = dynamic_cast(response);
+ if (!result)
+ return;
+
+ std::wstring log;
+ if (!LibertyXDR::utf8_to_wstring(
+ result->log.data(), result->log.size(), log)) {
+ emit errorOccurred("Invalid encoding.");
+ return;
+ }
+
+ std::vector linkified;
+ convert_links({}, QString::fromStdWString(log), linkified);
+
+ QTextDocument doc;
+ QTextCursor cursor(&doc);
+ for (const auto &it : linkified) {
+ cursor.setCharFormat(it.format);
+ cursor.insertText(it.text);
+ }
+
+ emit logViewChanged(doc.toHtml());
+ });
+}
+
+void
+RelayConnection::onSocketConnected()
+{
+ command_seq = 0;
+ command_callbacks.clear();
+
+ buffers.clear();
+ buffer_current.clear();
+ buffer_last.clear();
+ servers.clear();
+
+ buffer_list_model.refresh();
+
+ emit connectedChanged();
+ emit currentBufferChanged();
+ emit promptChanged();
+ emit statusChanged();
+ emit topicChanged();
+
+ auto hello = new Relay::CommandData_Hello();
+ hello->version = Relay::VERSION;
+ relaySend(hello);
+}
+
+void
+RelayConnection::onSocketDisconnected()
+{
+ emit connectedChanged();
+}
+
+void
+RelayConnection::onSocketReadyRead()
+{
+ union {
+ uint32_t frame_len = 0;
+ char buf[sizeof frame_len];
+ };
+ while (socket->peek(buf, sizeof buf) == sizeof buf) {
+ frame_len = qFromBigEndian(frame_len);
+ if (socket->bytesAvailable() < qint64(sizeof frame_len + frame_len))
+ break;
+
+ socket->skip(sizeof frame_len);
+ auto b = socket->read(frame_len);
+ LibertyXDR::Reader r;
+ r.data = reinterpret_cast(b.data());
+ r.length = b.size();
+
+ Relay::EventMessage m = {};
+ if (!m.deserialize(r) || r.length) {
+ emit errorOccurred("Deserialization failed");
+ socket->abort();
+ return;
+ }
+
+ processRelayEvent(m);
+ }
+}
+
+void
+RelayConnection::onSocketError(QAbstractSocket::SocketError error)
+{
+ Q_UNUSED(error);
+ emit errorOccurred(socket->errorString());
+}
+
+void
+RelayConnection::relaySend(Relay::CommandData *data, Callback callback)
+{
+ Relay::CommandMessage m = {};
+ m.command_seq = ++command_seq;
+ m.data.reset(data);
+ LibertyXDR::Writer w;
+ m.serialize(w);
+
+ if (callback)
+ command_callbacks[m.command_seq] = std::move(callback);
+ else
+ command_callbacks[m.command_seq] = [this](auto error, auto response) {
+ if (!response)
+ emit errorOccurred(QString::fromStdWString(error));
+ };
+
+ auto len = qToBigEndian(w.data.size());
+ auto prefix = reinterpret_cast(&len);
+ auto mdata = reinterpret_cast(w.data.data());
+ if (socket->write(prefix, sizeof len) < 0 ||
+ socket->write(mdata, w.data.size()) < 0) {
+ socket->abort();
+ }
+}
+
+void
+RelayConnection::processRelayEvent(const Relay::EventMessage &m)
+{
+ switch (m.data->event) {
+ case Relay::Event::ERROR:
+ {
+ auto data = dynamic_cast(m.data.get());
+ auto &callbacks = command_callbacks;
+ auto handler = callbacks.find(data->command_seq);
+ if (handler != callbacks.end()) {
+ if (handler->second)
+ handler->second(data->error, nullptr);
+ callbacks.erase(handler);
+ }
+ break;
+ }
+ case Relay::Event::RESPONSE:
+ {
+ auto data = dynamic_cast(m.data.get());
+ auto &callbacks = command_callbacks;
+ auto handler = callbacks.find(data->command_seq);
+ if (handler != callbacks.end()) {
+ if (handler->second)
+ handler->second({}, data->data.get());
+ callbacks.erase(handler);
+ }
+ break;
+ }
+
+ case Relay::Event::PING:
+ {
+ auto pong = new Relay::CommandData_PingResponse();
+ pong->event_seq = m.event_seq;
+ relaySend(pong);
+ break;
+ }
+
+ case Relay::Event::BUFFER_LINE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ BufferLine line = {};
+ line.items = convert_items(data.items);
+ line.is_unimportant = data.is_unimportant;
+ line.is_highlight = data.is_highlight;
+ line.rendition = data.rendition;
+ line.when = data.when;
+
+ b->lines.push_back(line);
+
+ // Update stats
+ auto bc = bufferByName(buffer_current);
+ if (bc) {
+ if (line.is_unimportant)
+ b->new_unimportant_messages++;
+ else
+ b->new_messages++;
+
+ if (line.is_highlight || (!line.is_unimportant &&
+ b->kind == Relay::BufferKind::PRIVATE_MESSAGE)) {
+ emit beepRequested();
+ b->highlighted = true;
+ }
+ }
+
+ buffer_list_model.refresh();
+ if (b->buffer_name == buffer_current) {
+ emit bufferTextChanged();
+ }
+ break;
+ }
+
+ case Relay::Event::BUFFER_UPDATE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b) {
+ buffers.push_back(Buffer());
+ b = &buffers.back();
+ b->buffer_name = QString::fromStdWString(data.buffer_name);
+ buffer_list_model.bufferAdded(buffers.size() - 1);
+ }
+
+ b->hide_unimportant = data.hide_unimportant;
+ b->kind = data.context->kind;
+ b->server_name.clear();
+
+ if (auto context = dynamic_cast(
+ data.context.get()))
+ b->server_name = QString::fromStdWString(context->server_name);
+ if (auto context = dynamic_cast(
+ data.context.get())) {
+ b->server_name = QString::fromStdWString(context->server_name);
+ b->modes = QString::fromStdWString(context->modes);
+ b->topic = convert_items(context->topic);
+ }
+ if (auto context = dynamic_cast(
+ data.context.get()))
+ b->server_name = QString::fromStdWString(context->server_name);
+
+ if (b->buffer_name == buffer_current) {
+ emit topicChanged();
+ emit statusChanged();
+ }
+ break;
+ }
+
+ case Relay::Event::BUFFER_STATS:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ b->new_messages = data.new_messages;
+ b->new_unimportant_messages = data.new_unimportant_messages;
+ b->highlighted = data.highlighted;
+
+ buffer_list_model.refresh();
+ refreshIcon();
+ break;
+ }
+
+ case Relay::Event::BUFFER_RENAME:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ auto original = b->buffer_name;
+ b->buffer_name = QString::fromStdWString(data.new_);
+
+ if (original == buffer_current) {
+ buffer_current = b->buffer_name;
+ emit currentBufferChanged();
+ emit statusChanged();
+ }
+ if (original == buffer_last)
+ buffer_last = b->buffer_name;
+
+ buffer_list_model.refresh();
+ break;
+ }
+
+ case Relay::Event::BUFFER_REMOVE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ int index = b - buffers.data();
+ buffer_list_model.bufferRemoved(index);
+ buffers.erase(buffers.begin() + index);
+ refreshIcon();
+ break;
+ }
+
+ case Relay::Event::BUFFER_ACTIVATE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto new_buffer = QString::fromStdWString(data.buffer_name);
+
+ // Signal QML to save current buffer's input before switching
+ emit aboutToChangeBuffer(buffer_current);
+
+ buffer_last = buffer_current;
+ buffer_current = new_buffer;
+ auto b = bufferByName(new_buffer);
+ if (!b)
+ break;
+
+ // Clear old buffer stats
+ if (auto old = bufferByName(buffer_last)) {
+ old->new_messages = 0;
+ old->new_unimportant_messages = 0;
+ old->highlighted = false;
+ }
+
+ b->highlighted = false;
+ buffer_list_model.setCurrentBuffer(buffer_current);
+ buffer_list_model.refresh();
+
+ refreshIcon();
+ emit currentBufferChanged();
+ emit topicChanged();
+ emit promptChanged();
+ emit statusChanged();
+ break;
+ }
+
+ case Relay::Event::BUFFER_INPUT:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ if (b->history_at == b->history.size())
+ b->history_at++;
+ b->history.push_back(
+ irc_to_rich_text(QString::fromStdWString(data.text)));
+ break;
+ }
+
+ case Relay::Event::BUFFER_CLEAR:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto b = bufferByName(QString::fromStdWString(data.buffer_name));
+ if (!b)
+ break;
+
+ b->lines.clear();
+ if (b->buffer_name == buffer_current) {
+ emit bufferTextChanged();
+ }
+ break;
+ }
+
+ case Relay::Event::SERVER_UPDATE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto name = QString::fromStdWString(data.server_name);
+ if (!servers.count(name))
+ servers.emplace(name, Server());
+
+ auto &server = servers.at(name);
+ server.state = data.data->state;
+
+ server.user.clear();
+ server.user_modes.clear();
+ if (auto registered = dynamic_cast(
+ data.data.get())) {
+ server.user = QString::fromStdWString(registered->user);
+ server.user_modes = QString::fromStdWString(registered->user_modes);
+ }
+
+ emit promptChanged();
+ break;
+ }
+
+ case Relay::Event::SERVER_RENAME:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto original = QString::fromStdWString(data.server_name);
+ servers.insert_or_assign(
+ QString::fromStdWString(data.new_), servers.at(original));
+ servers.erase(original);
+ break;
+ }
+
+ case Relay::Event::SERVER_REMOVE:
+ {
+ auto &data = dynamic_cast(*m.data);
+ auto name = QString::fromStdWString(data.server_name);
+ servers.erase(name);
+ break;
+ }
+ }
+}
+
+Buffer *
+RelayConnection::bufferByName(const QString &name)
+{
+ for (auto &b : buffers)
+ if (b.buffer_name == name)
+ return &b;
+ return nullptr;
+}
+
+void
+RelayConnection::refreshPrompt()
+{
+ emit promptChanged();
+}
+
+void
+RelayConnection::refreshStatus()
+{
+ emit statusChanged();
+}
+
+void
+RelayConnection::refreshTopic()
+{
+ emit topicChanged();
+}
+
+void
+RelayConnection::refreshIcon()
+{
+ // Window icon changes would need QML/Window integration
+ // For now, the icon highlighting is handled via buffer list colors
+}
+
+void
+RelayConnection::recheckHighlighted()
+{
+ // Highlight rechecking when scrolling to bottom
+ // This would be handled in QML when scroll position changes
+}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// --- Main --------------------------------------------------------------------
int
main(int argc, char *argv[])
diff --git a/xT/xTq.h b/xT/xTq.h
index 70a0374..343c836 100644
--- a/xT/xTq.h
+++ b/xT/xTq.h
@@ -1,15 +1,229 @@
#ifndef XTQ_H
#define XTQ_H
+#include
+#include
+#include
+#include