/*
* 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();
}