aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--xT/xTq.cpp1224
-rw-r--r--xT/xTq.h216
-rw-r--r--xT/xTq.qml413
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 <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 &current_)
+{
+ 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[])
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 <cstdint>
+#include <cstddef>
+#include <functional>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <QAbstractListModel>
+#include <QColor>
+#include <QHash>
+#include <QObject>
+#include <QString>
#include <QTcpSocket>
+#include <QTextCharFormat>
+#include <QTimer>
+#include <QVariant>
#include <QtQmlIntegration/qqmlintegration.h>
+#include <QQuickTextDocument>
+
+// Forward declarations for Relay protocol
+namespace Relay {
+ class CommandData;
+ class EventMessage;
+ class ResponseData;
+ enum class ServerState : int8_t;
+ enum class BufferKind : int8_t;
+ enum class Rendition : int8_t;
+ class ItemData;
+}
+
+// --- Data structures ---------------------------------------------------------
+
+struct Server {
+ Relay::ServerState state = {};
+ QString user;
+ QString user_modes;
+};
+
+struct BufferLineItem {
+ QTextCharFormat format = {};
+ QString text;
+};
+
+struct BufferLine {
+ /// Leaked from another buffer, but temporarily staying in another one.
+ bool leaked = {};
+
+ bool is_unimportant = {};
+ bool is_highlight = {};
+ Relay::Rendition rendition = {};
+ uint64_t when = {};
+ std::vector<BufferLineItem> items;
+};
+
+struct Buffer {
+ QString buffer_name;
+ bool hide_unimportant = {};
+ Relay::BufferKind kind = {};
+ QString server_name;
+ std::vector<BufferLine> lines;
+
+ // Channel:
+ std::vector<BufferLineItem> topic;
+ QString modes;
+
+ // Stats:
+ uint32_t new_messages = {};
+ uint32_t new_unimportant_messages = {};
+ bool highlighted = {};
+
+ // Input (stored as rich text):
+ QString input;
+ int input_start = {};
+ int input_end = {};
+ std::vector<QString> history;
+ size_t history_at = {};
+};
+
+using Callback = std::function<
+ void(std::wstring error, const Relay::ResponseData *response)>;
+
+// --- Buffer list model -------------------------------------------------------
+
+class BufferListModel : public QAbstractListModel {
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum Roles {
+ BufferNameRole = Qt::UserRole + 1,
+ BufferKindRole,
+ ServerNameRole,
+ NewMessagesRole,
+ HighlightedRole,
+ DisplayTextRole,
+ IsBoldRole,
+ HighlightColorRole
+ };
+
+ explicit BufferListModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ void setBuffers(const std::vector<Buffer> *buffers);
+ void setCurrentBuffer(const QString &current);
+ Q_INVOKABLE int getCurrentBufferIndex() const;
+ void refresh();
+ void refreshBuffer(int index);
+ void bufferAdded(int index);
+ void bufferRemoved(int index);
+
+signals:
+ void bufferActivated(const QString &bufferName);
+
+private:
+ const std::vector<Buffer> *buffers_ = nullptr;
+ QString current;
+};
+
+// --- Main relay connection ---------------------------------------------------
+
class RelayConnection : public QObject {
Q_OBJECT
QML_ELEMENT
+ Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged)
+ Q_PROPERTY(QString port READ port WRITE setPort NOTIFY portChanged)
+ Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged)
+ Q_PROPERTY(QString currentBuffer READ currentBuffer NOTIFY currentBufferChanged)
+ Q_PROPERTY(QString prompt READ prompt NOTIFY promptChanged)
+ Q_PROPERTY(QString status READ status NOTIFY statusChanged)
+ Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
+ Q_PROPERTY(BufferListModel *bufferListModel READ bufferListModel CONSTANT)
+
public:
- QTcpSocket *socket; ///< Buffered relay socket
+ explicit RelayConnection(QObject *parent = nullptr);
+
+ QString host() const { return host_; }
+ void setHost(const QString &host);
+
+ QString port() const { return port_; }
+ void setPort(const QString &port);
+
+ bool isConnected() const;
+
+ QString currentBuffer() const { return buffer_current; }
+ QString prompt() const;
+ QString status() const;
+ QString topic() const;
+
+ BufferListModel *bufferListModel() { return &buffer_list_model; }
+
+ Q_INVOKABLE void connectToRelay();
+ Q_INVOKABLE void disconnectFromRelay();
+ Q_INVOKABLE void activateBuffer(const QString &name);
+ Q_INVOKABLE void activatePreviousBuffer();
+ Q_INVOKABLE void activateNextBuffer();
+ Q_INVOKABLE void toggleUnimportant();
+ Q_INVOKABLE void sendInput(const QString &input);
+ Q_INVOKABLE void sendCommand(const QString &command);
+ Q_INVOKABLE void populateBufferDocument(QQuickTextDocument *document);
+ Q_INVOKABLE QString getInputHistoryUp();
+ Q_INVOKABLE QString getInputHistoryDown();
+ Q_INVOKABLE void requestCompletion(const QString &text, int position);
+ Q_INVOKABLE void activateLastBuffer();
+ Q_INVOKABLE void activateNextHighlighted();
+ Q_INVOKABLE void activateNextWithActivity();
+ Q_INVOKABLE void toggleBufferLog();
+ Q_INVOKABLE void saveBufferInput(const QString &bufferName, const QString &input, int start, int end);
+ Q_INVOKABLE QString getBufferInput();
+ Q_INVOKABLE int getBufferInputStart();
+ Q_INVOKABLE int getBufferInputEnd();
+
+signals:
+ void hostChanged();
+ void portChanged();
+ void connectedChanged();
+ void aboutToChangeBuffer(const QString &oldBuffer);
+ void currentBufferChanged();
+ void promptChanged();
+ void statusChanged();
+ void topicChanged();
+ void errorOccurred(const QString &message);
+ void beepRequested();
+ void completionResult(const QString &completion);
+ void bufferTextChanged();
+ void logViewChanged(const QString &logText);
+
+private slots:
+ void onSocketConnected();
+ void onSocketDisconnected();
+ void onSocketReadyRead();
+ void onSocketError(QAbstractSocket::SocketError error);
+
+private:
+ void relaySend(Relay::CommandData *data, Callback callback = {});
+ void processRelayEvent(const Relay::EventMessage &m);
+
+ Buffer *bufferByName(const QString &name);
+ void refreshPrompt();
+ void refreshStatus();
+ void refreshTopic();
+ void refreshIcon();
+ void recheckHighlighted();
+
+ QTcpSocket *socket;
+ QString host_;
+ QString port_;
+
+ uint32_t command_seq = 0;
+ std::map<uint32_t, Callback> command_callbacks;
+
+ std::vector<Buffer> buffers;
+ QString buffer_current;
+ QString buffer_last;
+ std::map<QString, Server> servers;
+
+ BufferListModel buffer_list_model;
+
+ QTimer *date_change_timer;
};
#endif // XTQ_H
diff --git a/xT/xTq.qml b/xT/xTq.qml
index 50063c9..ff6ba17 100644
--- a/xT/xTq.qml
+++ b/xT/xTq.qml
@@ -1,90 +1,422 @@
import QtQuick
import QtQuick.Controls.Fusion
-//import QtQuick.Controls
import QtQuick.Layouts
+import QtQuick.Dialogs
+import QtMultimedia
ApplicationWindow {
id: window
- width: 640
- height: 480
+ width: 960
+ height: 720
visible: true
- title: qsTr("xT")
+ title: qsTr("xTq")
- property RelayConnection connection
+ RelayConnection {
+ id: connection
+
+ onErrorOccurred: function(message) {
+ errorDialog.text = message
+ errorDialog.open()
+ }
+
+ onBeepRequested: {
+ beepSound.play()
+ }
+
+ onConnectedChanged: {
+ if (!connected) {
+ connectDialog.open()
+ }
+ }
+
+ onAboutToChangeBuffer: function(oldBuffer) {
+ // Save the current buffer's input before switching
+ connection.saveBufferInput(
+ oldBuffer,
+ inputText.text,
+ inputText.selectionStart,
+ inputText.selectionEnd
+ )
+ }
+
+ onBufferTextChanged: {
+ connection.populateBufferDocument(bufferText.textDocument)
+ scrollToBottom()
+ }
+
+ onCompletionResult: function(completion) {
+ inputText.text = completion
+ inputText.cursorPosition = inputText.text.length
+ }
+
+ onLogViewChanged: function(logText) {
+ logView.text = logText
+ logView.visible = true
+ bufferText.visible = false
+ logButton.checked = true
+ var vbar = bufferScroll.ScrollBar.vertical
+ vbar.position = 1.0 - vbar.size
+ }
+ }
+
+ SoundEffect {
+ id: beepSound
+ source: "qrc:/beep.wav"
+ volume: 0.5
+ }
ColumnLayout {
- id: column
anchors.fill: parent
anchors.margins: 6
+ spacing: 6
- ScrollView {
- id: bufferScroll
+ // Topic label
+ Label {
+ id: topicLabel
+ Layout.fillWidth: true
+ text: connection.topic
+ textFormat: Text.RichText
+ wrapMode: Text.Wrap
+ onLinkActivated: function(link) {
+ Qt.openUrlExternally(link)
+ }
+ visible: text.length > 0
+ padding: 4
+ background: Rectangle {
+ color: palette.base
+ border.color: palette.mid
+ border.width: 1
+ }
+ }
+
+ // Split view between buffer list and buffer display area
+ SplitView {
+ id: mainSplit
Layout.fillWidth: true
Layout.fillHeight: true
- TextArea {
- id: buffer
- text: qsTr("Buffer text")
+ orientation: Qt.Horizontal
+
+ // Buffer list
+ Rectangle {
+ SplitView.preferredWidth: 200
+ SplitView.minimumWidth: 150
+ color: "transparent"
+ border.color: palette.mid
+ border.width: 1
+
+ ListView {
+ id: bufferList
+ anchors.fill: parent
+ anchors.margins: 1
+ clip: true
+
+ model: connection.bufferListModel
+
+ Connections {
+ target: connection.bufferListModel
+ function onBufferActivated() {
+ bufferList.currentIndex = connection.bufferListModel.getCurrentBufferIndex()
+ }
+ }
+
+ delegate: ItemDelegate {
+ width: bufferList.width
+ text: model.displayText
+ font.bold: model.isBold
+ highlighted: ListView.isCurrentItem
+ contentItem: Label {
+ text: parent.text
+ font: parent.font
+ color: model.highlightColor.valid ?
+ model.highlightColor : palette.text
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ onClicked: {
+ bufferList.currentIndex = index
+ connection.activateBuffer(model.bufferName)
+ }
+ }
+
+ ScrollBar.vertical: ScrollBar {}
+ }
+ }
+
+ // Buffer display area
+ ScrollView {
+ id: bufferScroll
+ SplitView.fillWidth: true
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOn
+
+ TextArea {
+ id: bufferText
+ textFormat: Text.RichText
+ readOnly: true
+ wrapMode: Text.Wrap
+ selectByMouse: true
+ onLinkActivated: function(link) {
+ Qt.openUrlExternally(link)
+ }
+
+ Connections {
+ target: connection
+ function onCurrentBufferChanged() {
+ connection.populateBufferDocument(bufferText.textDocument)
+ bufferText.visible = true
+ logView.visible = false
+ logButton.checked = false
+ scrollToBottom()
+
+ // Restore input for the new buffer
+ inputText.text = connection.getBufferInput()
+ inputText.cursorPosition = connection.getBufferInputStart()
+ inputText.select(connection.getBufferInputStart(), connection.getBufferInputEnd())
+ }
+ }
+ }
+
+ TextArea {
+ id: logView
+ visible: false
+ textFormat: Text.RichText
+ readOnly: true
+ wrapMode: Text.Wrap
+ selectByMouse: true
+ font.family: "monospace"
+ onLinkActivated: function(link) {
+ Qt.openUrlExternally(link)
+ }
+ }
}
}
+ // Status area
RowLayout {
- id: row
Layout.fillWidth: true
+ spacing: 6
Label {
+ id: promptLabel
+ text: connection.prompt
+ }
+
+ Item {
Layout.fillWidth: true
- id: prompt
- text: qsTr("Prompt")
}
- Label {
+ ToolButton {
+ id: boldButton
+ text: "B"
+ font.bold: true
+ checkable: true
+ ToolTip.text: "Bold"
+ ToolTip.visible: hovered
+ onClicked: {
+ inputText.font.bold = checked
+ }
+ }
+
+ ToolButton {
+ id: italicButton
+ text: "I"
+ font.italic: true
+ checkable: true
+ ToolTip.text: "Italic"
+ ToolTip.visible: hovered
+ onClicked: {
+ inputText.font.italic = checked
+ }
+ }
+
+ ToolButton {
+ id: underlineButton
+ text: "U"
+ font.underline: true
+ checkable: true
+ ToolTip.text: "Underline"
+ ToolTip.visible: hovered
+ onClicked: {
+ inputText.font.underline = checked
+ }
+ }
+
+ Item {
Layout.fillWidth: true
- id: status
+ }
+
+ Label {
+ id: statusLabel
+ text: connection.status
horizontalAlignment: Text.AlignRight
- text: qsTr("Status")
+ }
+
+ ToolButton {
+ id: logButton
+ text: "Log"
+ checkable: true
+ ToolTip.text: "View buffer log"
+ ToolTip.visible: hovered
+ onClicked: {
+ if (checked) {
+ connection.toggleBufferLog()
+ } else {
+ logView.visible = false
+ bufferText.visible = true
+ logView.text = ""
+ }
+ }
+ }
+
+ ToolButton {
+ text: "↓"
+ ToolTip.text: "Scroll to bottom"
+ ToolTip.visible: hovered
+ enabled: {
+ var vbar = bufferScroll.ScrollBar.vertical
+ return vbar.position + vbar.size < 1.0
+ }
+ onClicked: scrollToBottom()
}
}
- TextArea {
- id: input
+ // Input area
+ ScrollView {
Layout.fillWidth: true
- text: qsTr("Input")
+ Layout.preferredHeight: Math.min(inputText.contentHeight + 12, 120)
+
+ TextArea {
+ id: inputText
+ wrapMode: Text.Wrap
+ selectByMouse: true
+ textFormat: Text.RichText
+
+ Keys.onReturnPressed: function(event) {
+ if (event.modifiers & Qt.ShiftModifier) {
+ // Allow Shift+Enter for newline
+ return
+ }
+ event.accepted = true
+ if (text.length > 0) {
+ connection.sendInput(text)
+ text = ""
+ }
+ }
+
+ Keys.onUpPressed: {
+ var history = connection.getInputHistoryUp()
+ if (history.length > 0) {
+ text = history
+ }
+ }
+
+ Keys.onDownPressed: {
+ var history = connection.getInputHistoryDown()
+ if (history.length > 0) {
+ text = history
+ }
+ }
+
+ Keys.onTabPressed: {
+ connection.requestCompletion(text, cursorPosition)
+ }
+ }
}
}
- Component.onCompleted: {}
+ // Keyboard shortcuts
+ Shortcut {
+ sequences: ["F5", "Alt+PgUp", "Ctrl+PgUp"]
+ onActivated: connection.activatePreviousBuffer()
+ }
+
+ Shortcut {
+ sequences: ["F6", "Alt+PgDown", "Ctrl+PgDown"]
+ onActivated: connection.activateNextBuffer()
+ }
+ Shortcut {
+ sequences: ["Ctrl+Tab", "Alt+Tab"]
+ onActivated: connection.activateLastBuffer()
+ }
+
+ Shortcut {
+ sequence: "Alt+A"
+ onActivated: connection.activateNextWithActivity()
+ }
+
+ Shortcut {
+ sequence: "Alt+!"
+ onActivated: connection.activateNextHighlighted()
+ }
+
+ Shortcut {
+ sequence: "Alt+H"
+ onActivated: connection.toggleUnimportant()
+ }
+
+ Shortcut {
+ sequence: "PgUp"
+ onActivated: {
+ var vbar = bufferScroll.ScrollBar.vertical
+ vbar.position = Math.max(0, vbar.position - vbar.size)
+ }
+ }
+
+ Shortcut {
+ sequence: "PgDown"
+ onActivated: {
+ var vbar = bufferScroll.ScrollBar.vertical
+ vbar.position = Math.min(1 - vbar.size, vbar.position + vbar.size)
+ }
+ }
+
+ // Helper function to scroll to bottom
+ function scrollToBottom() {
+ var vbar = bufferScroll.ScrollBar.vertical
+ vbar.position = 1.0 - vbar.size
+ }
+
+ // Connection dialog
Dialog {
- id: connect
+ id: connectDialog
title: "Connect to relay"
anchors.centerIn: parent
modal: true
- visible: true
+ closePolicy: Popup.NoAutoClose
+
+ onOpened: connectHost.forceActiveFocus()
- onRejected: Qt.quit()
onAccepted: {
- // TODO(p): Store the host, store the port, initiate connection.
+ connection.host = connectHost.text
+ connection.port = connectPort.text
+ connection.connectToRelay()
}
+ onRejected: Qt.quit()
+
GridLayout {
anchors.fill: parent
- anchors.margins: 6
columns: 2
- // It is a bit silly that one has to do everything manually.
- Keys.onReturnPressed: connect.accept()
-
Label { text: "Host:" }
TextField {
id: connectHost
Layout.fillWidth: true
- // And if this doesn't work reliably, do it after open().
+ text: connection.host || "localhost"
focus: true
+ selectByMouse: true
+ onAccepted: connectDialog.accept()
}
+
Label { text: "Port:" }
TextField {
id: connectPort
Layout.fillWidth: true
+ text: connection.port || ""
+ selectByMouse: true
+ validator: IntValidator { bottom: 0; top: 65535 }
+ onAccepted: connectDialog.accept()
}
}
@@ -92,14 +424,27 @@ ApplicationWindow {
Button {
text: qsTr("Connect")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
- Keys.onReturnPressed: connect.accept()
highlighted: true
}
Button {
- text: qsTr("Close")
- DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
- Keys.onReturnPressed: connect.reject()
+ text: qsTr("Exit")
+ DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
}
}
+
+ // Error dialog
+ MessageDialog {
+ id: errorDialog
+ title: "Error"
+ buttons: MessageDialog.Ok
+ }
+
+ Component.onCompleted: {
+ if (!connection.connected) {
+ connectDialog.open()
+ } else {
+ inputText.forceActiveFocus()
+ }
+ }
}