/* * xTq.cpp: Qt Quick frontend for xC * * Copyright (c) 2024, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #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 // --- 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[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); engine.loadFromModule("xTquick", "Main"); return app.exec(); }