diff options
Diffstat (limited to 'xT/xTq.cpp')
| -rw-r--r-- | xT/xTq.cpp | 1224 |
1 files changed, 1220 insertions, 4 deletions
@@ -17,15 +17,1231 @@ */ #include "xC-proto.cpp" +#include "xTq.h" -#include <cstdint> - +#include <QColor> +#include <QDateTime> #include <QGuiApplication> +#include <QLocale> #include <QQmlApplicationEngine> +#include <QQuickTextDocument> +#include <QRegularExpression> +#include <QTextBlock> +#include <QTextCursor> +#include <QTextDocument> +#include <QtDebug> +#include <QtEndian> +#include <QDateTime> +#include <QGuiApplication> +#include <QLocale> +#include <QQmlApplicationEngine> +#include <QRegularExpression> +#include <QTextBlock> +#include <QTextCursor> +#include <QTextDocument> +#include <QtDebug> +#include <QtEndian> -#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<Relay::ItemData_Reset *>(item)) { + cf = QTextCharFormat(); + inverse = false; + } else if (dynamic_cast<Relay::ItemData_FlipBold *>(item)) { + if (cf.fontWeight() <= QFont::Normal) + cf.setFontWeight(QFont::Bold); + else + cf.setFontWeight(QFont::Normal); + } else if (dynamic_cast<Relay::ItemData_FlipItalic *>(item)) { + cf.setFontItalic(!cf.fontItalic()); + } else if (dynamic_cast<Relay::ItemData_FlipUnderline *>(item)) { + cf.setFontUnderline(!cf.fontUnderline()); + } else if (dynamic_cast<Relay::ItemData_FlipCrossedOut *>(item)) { + cf.setFontStrikeOut(!cf.fontStrikeOut()); + } else if (dynamic_cast<Relay::ItemData_FlipInverse *>(item)) { + inverse = !inverse; + } else if (dynamic_cast<Relay::ItemData_FlipMonospace *>(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<Relay::ItemData_FgColor *>(item)) { + if (data->color < 0) { + cf.clearForeground(); + } else { + cf.setForeground(convert_color(data->color)); + } + } else if (auto data = dynamic_cast<Relay::ItemData_BgColor *>(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<BufferLineItem> &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<BufferLineItem> +convert_items(const std::vector<std::unique_ptr<Relay::ItemData>> &items) +{ + QTextCharFormat cf; + std::vector<BufferLineItem> result; + bool inverse = false; + for (const auto &it : items) { + auto text = dynamic_cast<Relay::ItemData_Text *>(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<int, QByteArray> +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<Buffer> *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<RelayConnection *>(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<RelayConnection *>(this)->bufferByName(buffer_current)) { + if (!b->modes.isEmpty()) + status += "(+" + b->modes + ")"; + if (b->hide_unimportant) + status += "<H>"; + } + return status; +} + +QString +RelayConnection::topic() const +{ + auto b = const_cast<RelayConnection *>(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<const Relay::ResponseData_BufferComplete *>(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<const Relay::ResponseData_BufferLog *>(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<BufferLineItem> 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<const uint8_t *>(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<uint32_t>(w.data.size()); + auto prefix = reinterpret_cast<const char *>(&len); + auto mdata = reinterpret_cast<const char *>(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<Relay::EventData_Error *>(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<Relay::EventData_Response *>(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<Relay::EventData_BufferLine &>(*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<Relay::EventData_BufferUpdate &>(*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<Relay::BufferContext_Server *>( + data.context.get())) + b->server_name = QString::fromStdWString(context->server_name); + if (auto context = dynamic_cast<Relay::BufferContext_Channel *>( + 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<Relay::BufferContext_PrivateMessage *>( + 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<Relay::EventData_BufferStats &>(*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<Relay::EventData_BufferRename &>(*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<Relay::EventData_BufferRemove &>(*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<Relay::EventData_BufferActivate &>(*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<Relay::EventData_BufferInput &>(*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<Relay::EventData_BufferClear &>(*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<Relay::EventData_ServerUpdate &>(*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<Relay::ServerData_Registered *>( + 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<Relay::EventData_ServerRename &>(*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<Relay::EventData_ServerRemove &>(*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[]) |
