xK/xT/xT.cpp
Přemysl Eric Janouch 6ac2ac5511
xT: improve MSYS2 build
The static Qt 6 package is unusable.
2024-12-19 03:08:13 +01:00

1725 lines
46 KiB
C++

/*
* xT.cpp: Qt frontend for xC
*
* Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
*
* 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 "config.h"
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include <QtEndian>
#include <QtDebug>
#include <QDateTime>
#include <QRegularExpression>
#include <QApplication>
#include <QFontDatabase>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMainWindow>
#include <QMessageBox>
#include <QPushButton>
#include <QScrollBar>
#include <QShortcut>
#include <QSplitter>
#include <QStackedWidget>
#include <QTextBrowser>
#include <QTextBlock>
#include <QTextEdit>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWindow>
#include <QSoundEffect>
#include <QTcpSocket>
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:
// The input is 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)>;
struct {
QMainWindow *wMain; ///< Main program window
QLabel *wTopic; ///< Channel topic
QListWidget *wBufferList; ///< Buffer list
QStackedWidget *wStack; ///< Buffer backlog/log stack
QTextBrowser *wBuffer; ///< Buffer backlog
QTextBrowser *wLog; ///< Buffer log
QLabel *wPrompt; ///< User name, etc.
QToolButton *wButtonB; ///< Toggle bold formatting
QToolButton *wButtonI; ///< Toggle italic formatting
QToolButton *wButtonU; ///< Toggle underlined formatting
QLabel *wStatus; ///< Buffer name, etc.
QToolButton *wButtonLog; ///< Buffer log toggle button
QToolButton *wButtonDown; ///< Scroll indicator
QTextEdit *wInput; ///< User input
QTimer *date_change_timer; ///< Timer for day changes
QLineEdit *wConnectHost; ///< Connection dialog: host
QLineEdit *wConnectPort; ///< Connection dialog: port
QDialog *wConnectDialog; ///< Connection details dialog
// Networking:
QString host; ///< Host as given by user
QString port; ///< Post/service as given by user
QTcpSocket *socket; ///< Buffered relay socket
// Relay protocol:
uint32_t command_seq; ///< Outgoing message counter
std::map<uint32_t, Callback> command_callbacks;
std::vector<Buffer> buffers; ///< List of all buffers
QString buffer_current; ///< Current buffer name or ""
QString buffer_last; ///< Previous buffer name or ""
std::map<QString, Server> servers;
} g;
static void
show_error_message(const QString &message)
{
QMessageBox::critical(g.wMain, "Error", message, QMessageBox::Ok);
}
static void
beep()
{
// We don't want to reuse the same instance.
auto *se = new QSoundEffect(g.wMain);
QObject::connect(se, &QSoundEffect::playingChanged, [=] {
if (!se->isPlaying())
se->deleteLater();
});
QObject::connect(se, &QSoundEffect::statusChanged, [=] {
if (se->status() == QSoundEffect::Error)
se->deleteLater();
});
se->setSource(QUrl("qrc:/beep.wav"));
se->setLoopCount(1);
se->setVolume(0.5);
se->play();
}
// --- Networking --------------------------------------------------------------
static void
relay_send(Relay::CommandData *data, Callback callback = {})
{
Relay::CommandMessage m = {};
m.command_seq = ++g.command_seq;
m.data.reset(data);
LibertyXDR::Writer w;
m.serialize(w);
if (callback)
g.command_callbacks[m.command_seq] = std::move(callback);
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 (g.socket->write(prefix, sizeof len) < 0 ||
g.socket->write(mdata, w.data.size()) < 0) {
g.socket->abort();
}
}
// --- Buffers -----------------------------------------------------------------
static Buffer *
buffer_by_name(const QString &name)
{
for (auto &b : g.buffers)
if (b.buffer_name == name)
return &b;
return nullptr;
}
static Buffer *
buffer_by_name(const std::wstring &name)
{
// The C++ LibertyXDR backend unfortunately targets Win32.
return buffer_by_name(QString::fromStdWString(name));
}
static bool
buffer_at_bottom()
{
auto sb = g.wBuffer->verticalScrollBar();
return sb->value() == sb->maximum();
}
static void
buffer_scroll_to_bottom()
{
auto sb = g.wBuffer->verticalScrollBar();
sb->setValue(sb->maximum());
}
// --- UI state refresh --------------------------------------------------------
static void
refresh_icon()
{
// This blocks Linux themes, but oh well.
QIcon icon(":/xT.png");
for (const auto &b : g.buffers)
if (b.highlighted)
icon = QIcon(":/xT-highlighted.png");
g.wMain->setWindowIcon(icon);
}
static void
textedit_replacesel(
QTextEdit *e, const QTextCharFormat &cf, const QString &text)
{
auto cursor = e->textCursor();
if (cf.fontFixedPitch()) {
auto fixed = QFontDatabase::systemFont(QFontDatabase::FixedFont);
auto adjusted = cf;
// For some reason, setting the families to empty also works.
adjusted.setFontFamilies(fixed.families());
cursor.setCharFormat(adjusted);
} else {
cursor.setCharFormat(cf);
}
cursor.insertText(text);
}
static void
refresh_topic(const std::vector<BufferLineItem> &topic)
{
QTextDocument doc;
QTextCursor cursor(&doc);
for (const auto &it : topic) {
cursor.setCharFormat(it.format);
cursor.insertText(it.text);
}
g.wTopic->setText(doc.toHtml());
}
static void
refresh_buffer_list_item(QListWidgetItem *item, const Buffer &b)
{
auto text = b.buffer_name;
QFont font;
QBrush color;
if (b.buffer_name != g.buffer_current && b.new_messages) {
text += " (" + QString::number(b.new_messages) + ")";
font.setBold(true);
}
if (b.highlighted)
color = QColor(0xff, 0x5f, 0x00);
item->setForeground(color);
item->setText(text);
item->setFont(font);
}
static void
refresh_buffer_list()
{
for (size_t i = 0; i < g.buffers.size(); i++)
refresh_buffer_list_item(g.wBufferList->item(i), g.buffers.at(i));
}
static QString
server_state_to_string(Relay::ServerState state)
{
switch (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 {};
}
static void
refresh_prompt()
{
QString prompt;
auto b = buffer_by_name(g.buffer_current);
if (!b) {
prompt = "Synchronizing...";
} else if (auto server = g.servers.find(b->server_name);
server != g.servers.end()) {
prompt = server->second.user;
if (!server->second.user_modes.isEmpty())
prompt += "(" + server->second.user_modes + ")";
if (prompt.isEmpty())
prompt = "(" + server_state_to_string(server->second.state) + ")";
}
g.wPrompt->setText(prompt);
}
static void
refresh_status()
{
g.wButtonDown->setEnabled(!buffer_at_bottom());
QString status = g.buffer_current;
if (auto b = buffer_by_name(g.buffer_current)) {
if (!b->modes.isEmpty())
status += "(+" + b->modes + ")";
if (b->hide_unimportant)
status += "<H>";
}
// Buffer scrolling would cause a ton of flickering redraws.
if (g.wStatus->text() != status)
g.wStatus->setText(status);
}
static void
recheck_highlighted()
{
// Corresponds to the logic toggling the bool on.
auto b = buffer_by_name(g.buffer_current);
if (b && b->highlighted && buffer_at_bottom() &&
!g.wMain->isMinimized() && !g.wLog->isVisible()) {
b->highlighted = false;
refresh_icon();
refresh_buffer_list();
}
}
// --- Buffer actions ----------------------------------------------------------
static void
buffer_activate(const QString &name)
{
auto activate = new Relay::CommandData_BufferActivate();
activate->buffer_name = name.toStdWString();
relay_send(activate);
}
static void
buffer_toggle_unimportant(const QString &name)
{
auto toggle = new Relay::CommandData_BufferToggleUnimportant();
toggle->buffer_name = name.toStdWString();
relay_send(toggle);
}
// FIXME: This works on the wrong level, we should take a vector and output
// a filtered vector--we must disregard individual items during URL matching.
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 void
buffer_toggle_log(
const std::wstring &error, const Relay::ResponseData_BufferLog *response)
{
if (!response) {
show_error_message(QString::fromStdWString(error));
return;
}
std::wstring log;
if (!LibertyXDR::utf8_to_wstring(
response->log.data(), response->log.size(), log)) {
show_error_message("Invalid encoding.");
return;
}
std::vector<BufferLineItem> linkified;
convert_links({}, QString::fromStdWString(log), linkified);
g.wButtonLog->setChecked(true);
g.wLog->setText({});
for (const auto &it : linkified)
textedit_replacesel(g.wLog, it.format, it.text);
g.wStack->setCurrentWidget(g.wLog);
// This triggers a relayout of some kind.
auto cursor = g.wLog->textCursor();
cursor.movePosition(QTextCursor::End);
g.wLog->setTextCursor(cursor);
auto sb = g.wLog->verticalScrollBar();
sb->setValue(sb->maximum());
}
static void
buffer_toggle_log()
{
if (g.wLog->isVisible()) {
g.wStack->setCurrentWidget(g.wBuffer);
g.wLog->setText("");
g.wButtonLog->setChecked(false);
recheck_highlighted();
return;
}
auto log = new Relay::CommandData_BufferLog();
log->buffer_name = g.buffer_current.toStdWString();
relay_send(log, [name = g.buffer_current](auto error, auto response) {
if (g.buffer_current != name)
return;
buffer_toggle_log(error,
dynamic_cast<const Relay::ResponseData_BufferLog *>(response));
});
}
// --- QTextEdit formatting ----------------------------------------------------
static QString
rich_text_to_irc(QTextEdit *textEdit)
{
QString irc;
for (auto block = textEdit->document()->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;
}
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();
cf.setFontFixedPitch((monospace = !monospace));
break;
case '\x1d':
apply();
cf.setFontItalic((italic = !italic));
break;
case '\x1e':
apply();
cf.setFontItalic((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 QBrush
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)) {
cf.setFontFixedPitch(!cf.fontFixedPitch());
} 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 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;
}
// --- Buffer output -----------------------------------------------------------
static BufferLine
convert_buffer_line(Relay::EventData_BufferLine &line)
{
BufferLine self = {};
self.items = convert_items(line.items);
self.is_unimportant = line.is_unimportant;
self.is_highlight = line.is_highlight;
self.rendition = line.rendition;
self.when = line.when;
return self;
}
static void
buffer_print_date_change(
bool &sameline, const QDateTime &last, const QDateTime &current)
{
if (last.date() == current.date())
return;
auto timestamp = current.toString(&"\n"[sameline] +
QLocale::system().dateFormat(QLocale::ShortFormat));
sameline = false;
QTextCharFormat cf;
cf.setFontWeight(QFont::Bold);
textedit_replacesel(g.wBuffer, cf, timestamp);
}
static bool
buffer_reset_selection()
{
auto sb = g.wBuffer->verticalScrollBar();
auto value = sb->value();
g.wBuffer->moveCursor(QTextCursor::End);
sb->setValue(value);
return g.wBuffer->textCursor().atBlockStart();
}
static void
buffer_print_and_watch_trailing_date_changes()
{
auto current = QDateTime::currentDateTime();
auto b = buffer_by_name(g.buffer_current);
if (b && !b->lines.empty()) {
auto last = QDateTime::fromMSecsSinceEpoch(b->lines.back().when);
bool sameline = buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
}
QDateTime midnight(current.date().addDays(1), {});
if (midnight < current)
return;
// Note that after printing the first trailing update,
// follow-up updates may be duplicated if timer events arrive too early.
g.date_change_timer->start(current.msecsTo(midnight) + 1);
}
static void
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
std::vector<BufferLine>::const_iterator line)
{
auto current = QDateTime::fromMSecsSinceEpoch(line->when);
auto last = line == begin ? QDateTime::currentDateTime()
: QDateTime::fromMSecsSinceEpoch((line - 1)->when);
bool sameline = buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
auto timestamp = current.toString(&"\nHH:mm:ss"[sameline]);
sameline = false;
QTextCharFormat cf;
cf.setForeground(QColor(0xbb, 0xbb, 0xbb));
cf.setBackground(QColor(0xf8, 0xf8, 0xf8));
textedit_replacesel(g.wBuffer, cf, timestamp);
cf = QTextCharFormat();
textedit_replacesel(g.wBuffer, cf, " ");
// Tabstops won't quite help us here, since we need it centred.
QString prefix;
QTextCharFormat pcf;
pcf.setFontFixedPitch(true);
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 (line->leaked) {
auto color = g.wBuffer->palette().color(
QPalette::Disabled, QPalette::Text);
pcf.setForeground(color);
if (!prefix.isEmpty()) {
textedit_replacesel(g.wBuffer, pcf, prefix);
}
for (auto it : line->items) {
it.format.setForeground(color);
it.format.clearBackground();
textedit_replacesel(g.wBuffer, it.format, it.text);
}
} else {
if (!prefix.isEmpty())
textedit_replacesel(g.wBuffer, pcf, prefix);
for (const auto &it : line->items)
textedit_replacesel(g.wBuffer, it.format, it.text);
}
}
static void
buffer_print_separator()
{
buffer_reset_selection();
QTextFrameFormat ff;
ff.setBackground(QColor(0xff, 0x5f, 0x00));
ff.setHeight(1);
// FIXME: When the current frame was empty, this seems to add a newline.
g.wBuffer->textCursor().insertFrame(ff);
}
static void
refresh_buffer(const Buffer &b)
{
g.wBuffer->clear();
size_t i = 0, mark_before = b.lines.size() -
b.new_messages - b.new_unimportant_messages;
for (auto line = b.lines.begin(); line != b.lines.end(); ++line) {
if (i == mark_before)
buffer_print_separator();
if (!line->is_unimportant || !b.hide_unimportant)
buffer_print_line(b.lines.begin(), line);
i++;
}
buffer_print_and_watch_trailing_date_changes();
buffer_scroll_to_bottom();
// TODO(p): recheck_highlighted() here, or do we handle enough signals?
}
// --- Event processing --------------------------------------------------------
static void
relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
{
// Initial sync: skip all other processing, let highlights be.
auto bc = buffer_by_name(g.buffer_current);
if (!bc) {
b.lines.push_back(convert_buffer_line(m));
return;
}
// Retained mode is complicated.
bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
(b.buffer_name == g.buffer_current || m.leak_to_active);
bool to_bottom = display && buffer_at_bottom();
bool visible = display &&
to_bottom &&
!g.wMain->isMinimized() &&
!g.wLog->isVisible();
bool separate = display &&
!visible && !bc->new_messages && !bc->new_unimportant_messages;
auto line = b.lines.insert(b.lines.end(), convert_buffer_line(m));
if (!(visible || m.leak_to_active) ||
b.new_messages || b.new_unimportant_messages) {
if (line->is_unimportant || m.leak_to_active)
b.new_unimportant_messages++;
else
b.new_messages++;
}
if (m.leak_to_active) {
auto line = bc->lines.insert(bc->lines.end(), convert_buffer_line(m));
line->leaked = true;
if (!visible || bc->new_messages || bc->new_unimportant_messages) {
if (line->is_unimportant)
bc->new_unimportant_messages++;
else
bc->new_messages++;
}
}
if (separate)
buffer_print_separator();
if (display)
buffer_print_line(bc->lines.begin(), bc->lines.end() - 1);
if (to_bottom)
buffer_scroll_to_bottom();
if (line->is_highlight || (!visible && !line->is_unimportant &&
b.kind == Relay::BufferKind::PRIVATE_MESSAGE)) {
beep();
if (!visible) {
b.highlighted = true;
refresh_icon();
}
}
refresh_buffer_list();
}
static void
relay_process_callbacks(uint32_t command_seq,
const std::wstring& error, const Relay::ResponseData *response)
{
auto &callbacks = g.command_callbacks;
auto handler = callbacks.find(command_seq);
if (handler == callbacks.end()) {
// TODO(p): Warn about an unawaited response.
} else {
if (handler->second)
handler->second(error, response);
callbacks.erase(handler);
}
// We don't particularly care about wraparound issues.
while (!callbacks.empty() && callbacks.begin()->first <= command_seq) {
auto front = callbacks.begin();
if (front->second)
front->second(L"No response", nullptr);
callbacks.erase(front);
}
}
static void
relay_process_message(const Relay::EventMessage &m)
{
switch (m.data->event) {
case Relay::Event::ERROR:
{
auto data = dynamic_cast<Relay::EventData_Error *>(m.data.get());
relay_process_callbacks(data->command_seq, data->error, nullptr);
break;
}
case Relay::Event::RESPONSE:
{
auto data = dynamic_cast<Relay::EventData_Response *>(m.data.get());
relay_process_callbacks(data->command_seq, {}, data->data.get());
break;
}
case Relay::Event::PING:
{
auto pong = new Relay::CommandData_PingResponse();
pong->event_seq = m.event_seq;
relay_send(pong);
break;
}
case Relay::Event::BUFFER_LINE:
{
auto &data = dynamic_cast<Relay::EventData_BufferLine &>(*m.data);
auto b = buffer_by_name(data.buffer_name);
if (!b)
break;
relay_process_buffer_line(*b, data);
break;
}
case Relay::Event::BUFFER_UPDATE:
{
auto &data = dynamic_cast<Relay::EventData_BufferUpdate &>(*m.data);
auto b = buffer_by_name(data.buffer_name);
if (!b) {
b = &*g.buffers.insert(g.buffers.end(), Buffer());
b->buffer_name = QString::fromStdWString(data.buffer_name);
auto item = new QListWidgetItem;
refresh_buffer_list_item(item, *b);
g.wBufferList->addItem(item);
}
bool hiding_toggled = b->hide_unimportant != data.hide_unimportant;
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 == g.buffer_current) {
refresh_topic(b->topic);
refresh_status();
if (hiding_toggled)
refresh_buffer(*b);
}
break;
}
case Relay::Event::BUFFER_STATS:
{
auto &data = dynamic_cast<Relay::EventData_BufferStats &>(*m.data);
auto b = buffer_by_name(data.buffer_name);
if (!b)
break;
b->new_messages = data.new_messages;
b->new_unimportant_messages = data.new_unimportant_messages;
b->highlighted = data.highlighted;
refresh_icon();
refresh_buffer_list();
break;
}
case Relay::Event::BUFFER_RENAME:
{
auto &data = dynamic_cast<Relay::EventData_BufferRename &>(*m.data);
auto b = buffer_by_name(data.buffer_name);
if (!b)
break;
auto original = b->buffer_name;
b->buffer_name = QString::fromStdWString(data.new_);
if (original == g.buffer_current) {
g.buffer_current = b->buffer_name;
refresh_status();
}
refresh_buffer_list();
if (original == g.buffer_last)
g.buffer_last = b->buffer_name;
break;
}
case Relay::Event::BUFFER_REMOVE:
{
auto &data = dynamic_cast<Relay::EventData_BufferRemove &>(*m.data);
auto b = buffer_by_name(data.buffer_name);
if (!b)
break;
int index = b - g.buffers.data();
delete g.wBufferList->takeItem(index);
g.buffers.erase(g.buffers.begin() + index);
refresh_icon();
break;
}
case Relay::Event::BUFFER_ACTIVATE:
{
auto &data = dynamic_cast<Relay::EventData_BufferActivate &>(*m.data);
Buffer *old = buffer_by_name(g.buffer_current);
g.buffer_last = g.buffer_current;
g.buffer_current = QString::fromStdWString(data.buffer_name);
auto b = buffer_by_name(data.buffer_name);
if (!b)
break;
if (old) {
old->new_messages = 0;
old->new_unimportant_messages = 0;
old->highlighted = false;
old->input = g.wInput->toHtml();
old->input_start = g.wInput->textCursor().selectionStart();
old->input_end = g.wInput->textCursor().selectionEnd();
// Note that we effectively overwrite the newest line
// with the current textarea contents, and jump there.
old->history_at = old->history.size();
}
if (g.wLog->isVisible())
buffer_toggle_log();
if (!g.wMain->isMinimized())
b->highlighted = false;
auto item = g.wBufferList->item(b - g.buffers.data());
refresh_buffer_list_item(item, *b);
g.wBufferList->setCurrentItem(item);
refresh_icon();
refresh_topic(b->topic);
refresh_buffer(*b);
refresh_prompt();
refresh_status();
g.wInput->setHtml(b->input);
g.wInput->textCursor().setPosition(b->input_start);
g.wInput->textCursor().setPosition(
b->input_end, QTextCursor::KeepAnchor);
g.wInput->setFocus();
break;
}
case Relay::Event::BUFFER_INPUT:
{
auto &data = dynamic_cast<Relay::EventData_BufferInput &>(*m.data);
auto b = buffer_by_name(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 = buffer_by_name(data.buffer_name);
if (!b)
break;
b->lines.clear();
if (b->buffer_name == g.buffer_current)
refresh_buffer(*b);
break;
}
case Relay::Event::SERVER_UPDATE:
{
auto &data = dynamic_cast<Relay::EventData_ServerUpdate &>(*m.data);
auto name = QString::fromStdWString(data.server_name);
if (!g.servers.count(name))
g.servers.emplace(name, Server());
auto &server = g.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);
}
refresh_prompt();
break;
}
case Relay::Event::SERVER_RENAME:
{
auto &data = dynamic_cast<Relay::EventData_ServerRename &>(*m.data);
auto original = QString::fromStdWString(data.server_name);
g.servers.insert_or_assign(
QString::fromStdWString(data.new_), g.servers.at(original));
g.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);
g.servers.erase(name);
break;
}
}
}
// --- Networking --------------------------------------------------------------
static void
relay_show_dialog()
{
g.wConnectHost->setText(g.host);
g.wConnectPort->setText(g.port);
g.wConnectDialog->move(
g.wMain->frameGeometry().center() - g.wConnectDialog->rect().center());
switch (g.wConnectDialog->exec()) {
case QDialog::Accepted:
g.host = g.wConnectHost->text();
g.port = g.wConnectPort->text();
g.socket->connectToHost(g.host, g.port.toUShort());
break;
case QDialog::Rejected:
QCoreApplication::exit();
}
}
static void
relay_process_error([[maybe_unused]] QAbstractSocket::SocketError err)
{
show_error_message(g.socket->errorString());
g.socket->abort();
QTimer::singleShot(0, relay_show_dialog);
}
static void
relay_process_connected()
{
g.command_seq = 0;
g.command_callbacks.clear();
g.buffers.clear();
g.buffer_current.clear();
g.buffer_last.clear();
g.servers.clear();
refresh_icon();
refresh_topic({});
g.wBufferList->clear();
g.wBuffer->clear();
refresh_prompt();
refresh_status();
auto hello = new Relay::CommandData_Hello();
hello->version = Relay::VERSION;
relay_send(hello);
}
static bool
relay_process_buffer(QString &error)
{
// How I wish I could access the internal read buffer directly.
auto s = g.socket;
union {
uint32_t frame_len = 0;
char buf[sizeof frame_len];
};
while (s->peek(buf, sizeof buf) == sizeof buf) {
frame_len = qFromBigEndian(frame_len);
if (s->bytesAvailable() < qint64(sizeof frame_len + frame_len))
break;
s->skip(sizeof frame_len);
auto b = s->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) {
error = "Deserialization failed.";
return false;
}
relay_process_message(m);
}
return true;
}
static void
relay_process_ready()
{
QString err;
if (!relay_process_buffer(err)) {
show_error_message(err);
g.socket->abort();
QTimer::singleShot(0, relay_show_dialog);
}
}
// --- Input line --------------------------------------------------------------
static void
input_set_contents(const QString &input)
{
g.wInput->setHtml(input);
auto cursor = g.wInput->textCursor();
cursor.movePosition(QTextCursor::End);
g.wInput->setTextCursor(cursor);
g.wInput->ensureCursorVisible();
}
static bool
input_submit()
{
auto b = buffer_by_name(g.buffer_current);
if (!b)
return false;
auto input = new Relay::CommandData_BufferInput();
input->buffer_name = b->buffer_name.toStdWString();
input->text = rich_text_to_irc(g.wInput).toStdWString();
// Buffer::history[Buffer::history.size()] is virtual,
// and is represented either by edit contents when it's currently
// being edited, or by Buffer::input in all other cases.
b->history.push_back(g.wInput->toHtml());
b->history_at = b->history.size();
input_set_contents("");
relay_send(input);
return true;
}
struct InputStamp {
int start = {};
int end = {};
QString input;
};
static InputStamp
input_stamp()
{
// Hopefully, the selection markers match the plain text characters.
auto start = g.wInput->textCursor().selectionStart();
auto end = g.wInput->textCursor().selectionEnd();
return {start, end, g.wInput->toPlainText()};
}
static void
input_complete(const InputStamp &state, const std::wstring &error,
const Relay::ResponseData_BufferComplete *response)
{
if (!response) {
show_error_message(QString::fromStdWString(error));
return;
}
auto utf8 = state.input.sliced(0, state.start).toUtf8();
auto preceding = QString(utf8.sliced(0, response->start));
if (response->completions.size() > 0) {
auto insert = response->completions.at(0);
if (response->completions.size() == 1)
insert += L" ";
auto cursor = g.wInput->textCursor();
cursor.setPosition(preceding.length());
cursor.setPosition(state.end, QTextCursor::KeepAnchor);
cursor.insertHtml(irc_to_rich_text(QString::fromStdWString(insert)));
}
if (response->completions.size() != 1)
beep();
// TODO(p): Show all completion options.
}
static bool
input_complete()
{
// TODO(p): Also add an increasing counter to the stamp.
auto state = input_stamp();
if (state.start != state.end)
return false;
auto utf8 = state.input.sliced(0, state.start).toUtf8();
auto complete = new Relay::CommandData_BufferComplete();
complete->buffer_name = g.buffer_current.toStdWString();
complete->text = state.input.toStdWString();
complete->position = utf8.size();
relay_send(complete, [state](auto error, auto response) {
auto stamp = input_stamp();
if (std::make_tuple(stamp.start, stamp.end, stamp.input) !=
std::make_tuple(state.start, state.end, state.input))
return;
input_complete(stamp, error,
dynamic_cast<const Relay::ResponseData_BufferComplete *>(response));
});
return true;
}
static bool
input_up()
{
auto b = buffer_by_name(g.buffer_current);
if (!b || b->history_at < 1)
return false;
if (b->history_at == b->history.size())
b->input = g.wInput->toHtml();
input_set_contents(b->history.at(--b->history_at));
return true;
}
static bool
input_down()
{
auto b = buffer_by_name(g.buffer_current);
if (!b || b->history_at >= b->history.size())
return false;
input_set_contents(++b->history_at == b->history.size()
? b->input
: b->history.at(b->history_at));
return true;
}
class InputEdit : public QTextEdit {
Q_OBJECT
public:
explicit InputEdit(QWidget *parent = nullptr) : QTextEdit(parent) {}
void keyPressEvent(QKeyEvent *event) override
{
auto scrollable = g.wLog->isVisible()
? g.wLog->verticalScrollBar()
: g.wBuffer->verticalScrollBar();
QKeyCombination combo(
event->modifiers() & ~Qt::KeypadModifier, Qt::Key(event->key()));
switch (combo.toCombined()) {
case Qt::Key_Return:
case Qt::Key_Enter:
input_submit();
break;
case QKeyCombination(Qt::ShiftModifier, Qt::Key_Return).toCombined():
case QKeyCombination(Qt::ShiftModifier, Qt::Key_Enter).toCombined():
// Qt amazingly inserts U+2028 LINE SEPARATOR instead.
this->textCursor().insertText("\n");
break;
case Qt::Key_Tab:
input_complete();
break;
case QKeyCombination(Qt::AltModifier, Qt::Key_P).toCombined():
case Qt::Key_Up:
input_up();
break;
case QKeyCombination(Qt::AltModifier, Qt::Key_N).toCombined():
case Qt::Key_Down:
input_down();
break;
case Qt::Key_PageUp:
scrollable->setValue(scrollable->value() - scrollable->pageStep());
break;
case Qt::Key_PageDown:
scrollable->setValue(scrollable->value() + scrollable->pageStep());
break;
default:
QTextEdit::keyPressEvent(event);
return;
}
event->accept();
}
};
// --- General UI --------------------------------------------------------------
class BufferEdit : public QTextBrowser {
Q_OBJECT
public:
explicit BufferEdit(QWidget *parent = nullptr) : QTextBrowser(parent) {}
void resizeEvent(QResizeEvent *event) override
{
bool to_bottom = buffer_at_bottom();
QTextBrowser::resizeEvent(event);
if (to_bottom) {
buffer_scroll_to_bottom();
} else {
recheck_highlighted();
refresh_status();
}
}
};
static void
build_main_window()
{
g.wMain = new QMainWindow;
refresh_icon();
auto central = new QWidget(g.wMain);
auto vbox = new QVBoxLayout(central);
vbox->setContentsMargins(4, 4, 4, 4);
g.wTopic = new QLabel(central);
g.wTopic->setTextFormat(Qt::RichText);
vbox->addWidget(g.wTopic);
auto splitter = new QSplitter(Qt::Horizontal, central);
splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
g.wBufferList = new QListWidget(splitter);
g.wBufferList->setSizePolicy(
QSizePolicy::Preferred, QSizePolicy::Expanding);
QObject::connect(g.wBufferList, &QListWidget::currentRowChanged,
[](int row) {
if (row >= 0 && (size_t) row < g.buffers.size())
buffer_activate(g.buffers.at(row).buffer_name);
});
g.wStack = new QStackedWidget(splitter);
g.wBuffer = new BufferEdit(g.wStack);
g.wBuffer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
g.wBuffer->setReadOnly(true);
g.wBuffer->setTextInteractionFlags(
Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard |
Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
g.wBuffer->setOpenExternalLinks(true);
QObject::connect(g.wBuffer->verticalScrollBar(), &QScrollBar::valueChanged,
[]([[maybe_unused]] int value) {
recheck_highlighted();
refresh_status();
});
QObject::connect(g.wBuffer->verticalScrollBar(), &QScrollBar::rangeChanged,
[]([[maybe_unused]] int min, [[maybe_unused]] int max) {
recheck_highlighted();
refresh_status();
});
g.wLog = new QTextBrowser(g.wStack);
g.wLog->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
g.wLog->setReadOnly(true);
g.wLog->setTextInteractionFlags(
Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard |
Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
g.wLog->setOpenExternalLinks(true);
g.wStack->addWidget(g.wBuffer);
g.wStack->addWidget(g.wLog);
splitter->addWidget(g.wBufferList);
splitter->setStretchFactor(0, 1);
splitter->addWidget(g.wStack);
splitter->setStretchFactor(1, 2);
vbox->addWidget(splitter);
auto hbox = new QHBoxLayout();
g.wPrompt = new QLabel(central);
hbox->addWidget(g.wPrompt);
g.wButtonB = new QToolButton(central);
g.wButtonB->setText("&B");
g.wButtonB->setCheckable(true);
hbox->addWidget(g.wButtonB);
g.wButtonI = new QToolButton(central);
g.wButtonI->setText("&I");
g.wButtonI->setCheckable(true);
hbox->addWidget(g.wButtonI);
g.wButtonU = new QToolButton(central);
g.wButtonU->setText("&U");
g.wButtonU->setCheckable(true);
hbox->addWidget(g.wButtonU);
g.wStatus = new QLabel(central);
g.wStatus->setAlignment(
Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
hbox->addWidget(g.wStatus);
g.wButtonLog = new QToolButton(central);
g.wButtonLog->setText("&Log");
g.wButtonLog->setCheckable(true);
QObject::connect(g.wButtonLog, &QToolButton::clicked,
[]([[maybe_unused]] bool checked) { buffer_toggle_log(); });
hbox->addWidget(g.wButtonLog);
g.wButtonDown = new QToolButton(central);
g.wButtonDown->setIcon(
QApplication::style()->standardIcon(QStyle::SP_ArrowDown));
g.wButtonDown->setToolButtonStyle(Qt::ToolButtonIconOnly);
QObject::connect(g.wButtonDown, &QToolButton::clicked,
[]([[maybe_unused]] bool checked) { buffer_scroll_to_bottom(); });
hbox->addWidget(g.wButtonDown);
vbox->addLayout(hbox);
g.wInput = new InputEdit(central);
g.wInput->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
g.wInput->setMaximumHeight(50);
vbox->addWidget(g.wInput);
// TODO(p): Figure out why this is not reliable.
QObject::connect(g.wInput, &QTextEdit::currentCharFormatChanged,
[](const QTextCharFormat &format) {
g.wButtonB->setChecked(format.fontWeight() >= QFont::Bold);
g.wButtonI->setChecked(format.fontItalic());
g.wButtonU->setChecked(format.fontUnderline());
});
QObject::connect(g.wButtonB, &QToolButton::clicked,
[](bool checked) {
auto cursor = g.wInput->textCursor();
auto format = cursor.charFormat();
format.setFontWeight(checked ? QFont::Bold : QFont::Normal);
cursor.mergeCharFormat(format);
g.wInput->setTextCursor(cursor);
});
QObject::connect(g.wButtonI, &QToolButton::clicked,
[](bool checked) {
auto cursor = g.wInput->textCursor();
auto format = cursor.charFormat();
format.setFontItalic(checked);
cursor.mergeCharFormat(format);
g.wInput->setTextCursor(cursor);
});
QObject::connect(g.wButtonU, &QToolButton::clicked,
[](bool checked) {
auto cursor = g.wInput->textCursor();
auto format = cursor.charFormat();
format.setFontUnderline(checked);
cursor.mergeCharFormat(format);
g.wInput->setTextCursor(cursor);
});
central->setLayout(vbox);
g.wMain->setCentralWidget(central);
g.wMain->show();
}
static void
build_connect_dialog()
{
auto dialog = g.wConnectDialog = new QDialog(g.wMain);
dialog->setModal(true);
dialog->setWindowTitle("Connect to relay");
auto layout = new QFormLayout();
g.wConnectHost = new QLineEdit(dialog);
layout->addRow("&Host:", g.wConnectHost);
g.wConnectPort = new QLineEdit(dialog);
auto validator = new QIntValidator(0, 0xffff, g.wConnectDialog);
g.wConnectPort->setValidator(validator);
layout->addRow("&Port:", g.wConnectPort);
auto buttons = new QDialogButtonBox(dialog);
buttons->addButton(new QPushButton("&Connect", buttons),
QDialogButtonBox::AcceptRole);
buttons->addButton(new QPushButton("&Exit", buttons),
QDialogButtonBox::RejectRole);
QObject::connect(buttons, &QDialogButtonBox::accepted,
dialog, &QDialog::accept);
QObject::connect(buttons, &QDialogButtonBox::rejected,
dialog, &QDialog::reject);
auto vbox = new QVBoxLayout();
vbox->addLayout(layout);
vbox->addWidget(buttons);
dialog->setLayout(vbox);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static std::vector<size_t>
rotated_buffers()
{
std::vector<size_t> rotated(g.buffers.size());
size_t start = 0;
for (auto it = g.buffers.begin(); it != g.buffers.end(); ++it)
if (it->buffer_name == g.buffer_current) {
start = it - g.buffers.begin();
break;
}
for (auto &index : rotated)
index = ++start % g.buffers.size();
return rotated;
}
static void
bind_shortcuts()
{
auto previous_buffer = [] {
auto rotated = rotated_buffers();
if (rotated.size() > 0) {
size_t i = (rotated.back() ? rotated.back() : g.buffers.size()) - 1;
buffer_activate(g.buffers[i].buffer_name);
}
};
auto next_buffer = [] {
auto rotated = rotated_buffers();
if (rotated.size() > 0)
buffer_activate(g.buffers[rotated.front()].buffer_name);
};
auto switch_buffer = [] {
if (auto b = buffer_by_name(g.buffer_last))
buffer_activate(b->buffer_name);
};
auto goto_highlight = [] {
for (auto i : rotated_buffers())
if (g.buffers[i].highlighted) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
};
auto goto_activity = [] {
for (auto i : rotated_buffers())
if (g.buffers[i].new_messages) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
};
auto toggle_unimportant = [] {
if (auto b = buffer_by_name(g.buffer_current))
buffer_toggle_unimportant(b->buffer_name);
};
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_Tab),
g.wMain, switch_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Tab),
g.wMain, switch_buffer);
new QShortcut(QKeyCombination(Qt::NoModifier, Qt::Key_F5),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_PageUp),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_PageUp),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::NoModifier, Qt::Key_F6),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_PageDown),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_PageDown),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_A),
g.wMain, goto_activity);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Exclam),
g.wMain, goto_highlight);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_H),
g.wMain, toggle_unimportant);
}
int
main(int argc, char *argv[])
{
QApplication app(argc, argv);
auto args = app.arguments();
if (args.size() != 1 && args.size() != 3) {
QMessageBox::critical(nullptr, "Error", "Usage: xT [HOST PORT]",
QMessageBox::Close);
return 1;
}
build_main_window();
build_connect_dialog();
bind_shortcuts();
g.date_change_timer = new QTimer(g.wMain);
g.date_change_timer->setSingleShot(true);
QObject::connect(g.date_change_timer, &QTimer::timeout, [] {
bool to_bottom = buffer_at_bottom();
buffer_print_and_watch_trailing_date_changes();
if (to_bottom)
buffer_scroll_to_bottom();
});
g.socket = new QTcpSocket(g.wMain);
QObject::connect(g.socket, &QTcpSocket::errorOccurred,
relay_process_error);
QObject::connect(g.socket, &QTcpSocket::connected,
relay_process_connected);
QObject::connect(g.socket, &QTcpSocket::readyRead,
relay_process_ready);
if (args.size() == 3) {
g.host = args[1];
g.port = args[2];
g.socket->connectToHost(g.host, g.port.toUShort());
} else {
// Allow it to center on its parent, which must be realized.
while (!g.wMain->windowHandle()->isExposed())
app.processEvents();
QTimer::singleShot(0, relay_show_dialog);
}
int result = app.exec();
delete g.wMain;
return result;
}
// Normally, QObjects should be placed in header files, which we don't do.
#include "xT.moc"