orkmode/ui/src/main.cpp

157 lines
6.1 KiB
C++

// orkmode GUI entry point.
//
// A read-only org-mode document viewer that speaks 9P to ork-server. The
// server address is taken from --addr, then $ORK_ADDR, defaulting to
// tcp://localhost:5640 (ork-server's own default). No org logic lives here;
// the GUI walks the server's namespace and renders it.
#include "documentmodel.h"
#include "orkclient.h"
#include "thememanager.h"
#include <QGuiApplication>
#include <QImage>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
#include <QTimer>
#include <QtQml>
#include <cstdio>
// Walk the model recursively and print each section, for --selftest.
static void dumpModel(const DocumentModel &m, const QModelIndex &parent, int depth) {
const int rows = m.rowCount(parent);
for (int r = 0; r < rows; ++r) {
const QModelIndex idx = m.index(r, 0, parent);
const QString kw = m.data(idx, DocumentModel::KeywordRole).toString();
const QString pri = m.data(idx, DocumentModel::PriorityRole).toString();
const QString title = m.data(idx, DocumentModel::TitleRole).toString();
const QStringList tags = m.data(idx, DocumentModel::TagsRole).toStringList();
const QString sched = m.data(idx, DocumentModel::ScheduledRole).toString();
const QString dead = m.data(idx, DocumentModel::DeadlineRole).toString();
QString line = QString(depth * 2, ' ') + "* ";
if (!kw.isEmpty()) line += kw + " ";
if (!pri.isEmpty()) line += "[#" + pri + "] ";
line += title;
if (!tags.isEmpty()) line += " :" + tags.join(':') + ":";
if (!sched.isEmpty()) line += " SCHED=" + sched;
if (!dead.isEmpty()) line += " DEAD=" + dead;
std::fprintf(stdout, "%s\n", qUtf8Printable(line));
const QString body = m.data(idx, DocumentModel::BodyRole).toString();
if (!body.trimmed().isEmpty()) {
const QString indent = QString(depth * 2 + 4, ' ');
for (const QString &bl : body.split('\n')) {
std::fprintf(stdout, "%s| %s\n", qUtf8Printable(indent),
qUtf8Printable(bl));
}
}
// Force lazy child load then recurse.
if (m.canFetchMore(idx)) const_cast<DocumentModel &>(m).fetchMore(idx);
dumpModel(m, idx, depth + 1);
}
}
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
app.setApplicationName("orkmode");
app.setOrganizationName("orkmode");
// Resolve the server address: --addr, else $ORK_ADDR, else default.
QString addr = QStringLiteral("tcp://localhost:5640");
if (const QByteArray env = qgetenv("ORK_ADDR"); !env.isEmpty()) {
addr = QString::fromUtf8(env);
}
const QStringList args = app.arguments();
bool selftest = false;
QString shotPath;
for (int i = 1; i < args.size(); ++i) {
if ((args[i] == "-a" || args[i] == "--addr") && i + 1 < args.size()) {
addr = args[i + 1];
++i;
} else if (args[i] == "--selftest") {
selftest = true;
} else if (args[i] == "--shot" && i + 1 < args.size()) {
shotPath = args[i + 1];
++i;
}
}
// Expose the client, document model, and theme manager to QML.
OrkClient client;
DocumentModel docModel;
ThemeManager theme;
docModel.setClient(&client);
const bool connected = client.connectTo(addr);
if (!connected) {
qWarning("orkmode: could not connect to %s: %s",
qUtf8Printable(addr), qUtf8Printable(client.error()));
// Continue anyway: the QML shows the connection error and offers retry.
}
// Headless verification: dump the doc list and each document's outline via
// the model, then exit. Used to test the 9P client + model without a GUI.
if (selftest) {
if (!connected) {
std::fprintf(stderr, "selftest: not connected: %s\n",
qUtf8Printable(client.error()));
return 2;
}
const QVariantList docs = client.docs();
std::fprintf(stdout, "documents: %lld\n",
static_cast<long long>(docs.size()));
for (const QVariant &d : docs) {
const QVariantMap m = d.toMap();
const QString name = m.value("name").toString();
std::fprintf(stdout, "== %s (%s) ==\n",
qUtf8Printable(m.value("title").toString()),
qUtf8Printable(name));
docModel.setDocument(name);
dumpModel(docModel, QModelIndex(), 0);
}
return docs.isEmpty() ? 1 : 0;
}
QObject::connect(&client, &OrkClient::connectedChanged, &docModel,
[&docModel]() { docModel.reload(); });
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("orkClient", &client);
engine.rootContext()->setContextProperty("docModel", &docModel);
engine.rootContext()->setContextProperty("theme", &theme);
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[]() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
engine.loadFromModule("orkmode", "Main");
if (engine.rootObjects().isEmpty()) return -1;
// Screenshot mode: after the scene has rendered, grab the window to a PNG
// and quit. Used to verify layout without a display.
if (!shotPath.isEmpty()) {
auto *win = qobject_cast<QQuickWindow *>(engine.rootObjects().first());
if (!win) {
std::fprintf(stderr, "shot: root object is not a window\n");
return 3;
}
// Grab after a short delay so async model reads and layout settle.
QTimer::singleShot(800, &app, [&, shotPath]() {
QImage img = win->grabWindow();
if (!img.isNull() && img.save(shotPath)) {
std::fprintf(stdout, "shot saved: %s (%dx%d)\n",
qUtf8Printable(shotPath), img.width(), img.height());
} else {
std::fprintf(stderr, "shot: failed to save %s\n",
qUtf8Printable(shotPath));
}
QCoreApplication::quit();
});
}
return app.exec();
}