Stage history editor: - The edit form's Pipeline section is now the application's history: one row per step (stage + date), Add Step and Remove Step, kept in date order. The last step is the current stage and the first step's date the date applied, replacing the separate Stage and Date Applied fields. An application logged after the fact (applied, interviewed, rejected) keeps its whole path. - JobsDatabase::replaceStageHistory() rewrites a job's history in one transaction (sorted, repeats collapsed, stage and date applied kept in step); StageHistoryModel backs the section; JobsModel::addJob() now returns the new id so a new application's history can be saved. - `kareer history <id> [Stage=YYYY-MM-DD ...]` shows or replaces it. Auto-ghosting: - Applications still at Applied with no activity (the later of the date applied and the last stage change) for more than 30 days move to Ghosted, recorded like any stage change. Runs at startup for GUI and CLI, after switching databases, and shortly after the setting changes. - Preferences gains an Applications section: on/off and the number of days (kareerrc [AutoGhost]). The GUI shows a passive notification; the CLI notes it on stderr so --json output stays clean. Fixes: - The Add Application form pre-filled empty text fields with the word "undefined" (typing "a" gave "undefineda"): fields with no value reached QML as undefined. Every field now gets a typed default. - Embed the app icon as the window icon fallback, so the window and About page show it when running uninstalled. Tests: history replacement and the after-the-fact Allstate case, the ghosting rules (fresh/stale/logged-late/reopened/threshold), and empty new-form fields. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01BxDf7HqD1xPnsZP8wt3NTk
185 lines
6.6 KiB
C++
185 lines
6.6 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
|
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include "kareer-version.h"
|
|
|
|
#include "autoghost.h"
|
|
#include "clicommands.h"
|
|
#include "databaselocation.h"
|
|
#include "jobsdatabase.h"
|
|
|
|
#include <KAboutData>
|
|
#include <KCrash>
|
|
#include <KIconTheme>
|
|
#include <KLocalizedQmlContext>
|
|
#include <KLocalizedString>
|
|
|
|
#include <QApplication>
|
|
#include <QCommandLineParser>
|
|
#include <QCoreApplication>
|
|
#include <QFileInfo>
|
|
#include <QIcon>
|
|
#include <QQmlApplicationEngine>
|
|
#include <QQuickStyle>
|
|
|
|
using namespace Qt::Literals::StringLiterals;
|
|
|
|
// Filter out a couple of well-known benign framework artifacts rather than
|
|
// spamming every run. Everything else is passed through untouched.
|
|
static bool isBenignFrameworkNoise(const QString &message)
|
|
{
|
|
// Qt Quick emits this while Kirigami's PageRow incubates pages. It fires even
|
|
// for a trivial empty page, is harmless, and cannot be avoided from app code.
|
|
if (message.contains(QLatin1String("was not placed in the graphics scene"))) {
|
|
return true;
|
|
}
|
|
// An internal PageRow/StackView implementation detail, not something app
|
|
// code can influence.
|
|
if (message.contains(QLatin1String("StackView has detected conflicting anchors"))) {
|
|
return true;
|
|
}
|
|
// Qt's Wayland integration tries to self-register with xdg-desktop-portal for
|
|
// optional desktop features (global shortcuts, background). Kareer doesn't use
|
|
// any of those, and it fires harmlessly on hosts where portal app-info
|
|
// resolution is finicky.
|
|
if (message.contains(QLatin1String("Failed to register with host portal"))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static QtMessageHandler s_defaultMessageHandler = nullptr;
|
|
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
|
{
|
|
if (isBenignFrameworkNoise(message)) {
|
|
return;
|
|
}
|
|
// Malformed path data in third-party icon SVGs (system/app icon themes) is not
|
|
// actionable from here; drop the noise rather than spam every render.
|
|
if (context.category && qstrcmp(context.category, "qt.svg") == 0) {
|
|
return;
|
|
}
|
|
if (s_defaultMessageHandler) {
|
|
s_defaultMessageHandler(type, context, message);
|
|
}
|
|
}
|
|
|
|
/// Removes "--db <path>" / "--db=<path>" from argv (anywhere on the command
|
|
/// line, for both the GUI and every subcommand) and applies it as the
|
|
/// database override. Stripping it up front keeps "kareer --db x list"
|
|
/// routing to the CLI. Returns false if --db is missing its value.
|
|
static bool takeDbOption(int &argc, char **argv)
|
|
{
|
|
int out = 1;
|
|
for (int in = 1; in < argc; ++in) {
|
|
const QString arg = QString::fromLocal8Bit(argv[in]);
|
|
QString value;
|
|
if (arg == u"--db"_s) {
|
|
if (in + 1 >= argc) {
|
|
fprintf(stderr, "kareer: --db requires a path\n");
|
|
return false;
|
|
}
|
|
value = QString::fromLocal8Bit(argv[++in]);
|
|
} else if (arg.startsWith(u"--db="_s)) {
|
|
value = arg.mid(5);
|
|
} else {
|
|
argv[out++] = argv[in];
|
|
continue;
|
|
}
|
|
if (value.isEmpty()) {
|
|
fprintf(stderr, "kareer: --db requires a path\n");
|
|
return false;
|
|
}
|
|
JobsDatabase::setPathOverride(QFileInfo(value).absoluteFilePath());
|
|
}
|
|
argv[out] = nullptr;
|
|
argc = out;
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
s_defaultMessageHandler = qInstallMessageHandler(messageHandler);
|
|
|
|
if (!takeDbOption(argc, argv)) {
|
|
return 1;
|
|
}
|
|
|
|
if (argc >= 2 && Cli::isSubcommand(QString::fromLocal8Bit(argv[1]))) {
|
|
QCoreApplication app(argc, argv);
|
|
DatabaseLocation::loadConfiguredPath();
|
|
if (const int ghosted = AutoGhost::runNow(); ghosted > 0) {
|
|
// stderr, so --json output on stdout stays machine-readable.
|
|
fprintf(stderr, "kareer: marked %d application(s) with no response as Ghosted\n", ghosted);
|
|
}
|
|
return Cli::run(app);
|
|
}
|
|
|
|
KIconTheme::initTheme();
|
|
|
|
QApplication app(argc, argv);
|
|
KLocalizedString::setApplicationDomain(QByteArrayLiteral("kareer"));
|
|
QCoreApplication::setOrganizationName(u"toservetheking"_s);
|
|
|
|
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
|
QQuickStyle::setStyle(u"org.kde.desktop"_s);
|
|
QQuickStyle::setFallbackStyle(u"Fusion"_s);
|
|
}
|
|
|
|
KAboutData aboutData(u"kareer"_s,
|
|
i18nc("@title", "Kareer"),
|
|
QStringLiteral(KAREER_VERSION_STRING),
|
|
i18n("Track your job applications"),
|
|
KAboutLicense::GPL_V3,
|
|
i18n("© 2026 Kareer contributors"));
|
|
aboutData.addAuthor(u"toservetheking"_s, i18nc("@label", "Author"), u"[email protected]"_s);
|
|
aboutData.setDesktopFileName(u"io.github.toservetheking.Kareer"_s);
|
|
KAboutData::setApplicationData(aboutData);
|
|
|
|
QApplication::setWindowIcon(QIcon::fromTheme(u"io.github.toservetheking.Kareer"_s, QIcon(u":/icons/sc-apps-io.github.toservetheking.Kareer.svg"_s)));
|
|
|
|
KCrash::initialize();
|
|
|
|
QCommandLineParser parser;
|
|
aboutData.setupCommandLine(&parser);
|
|
// Handled (and stripped) by takeDbOption(); declared here for --help.
|
|
parser.addOption(QCommandLineOption(u"db"_s, i18n("Use this database file instead of the configured one."), i18n("path")));
|
|
parser.process(app);
|
|
aboutData.processCommandLine(&parser);
|
|
|
|
DatabaseLocation::loadConfiguredPath();
|
|
// First run (or the configured file has gone missing): open nothing until
|
|
// the user picks a location in DatabaseSetupDialog. The CLI never waits.
|
|
JobsDatabase::setSelectionPending(DatabaseLocation::needsSetup());
|
|
if (!JobsDatabase::selectionPending()) {
|
|
AutoGhost::runNow(); // before the models load; Main.qml reports the count
|
|
}
|
|
|
|
QQmlApplicationEngine engine;
|
|
KLocalization::setupLocalizedContext(&engine);
|
|
|
|
QObject::connect(&engine, &QQmlApplicationEngine::warnings, &engine, [](const QList<QQmlError> &warnings) {
|
|
for (const QQmlError &error : warnings) {
|
|
if (isBenignFrameworkNoise(error.description())) {
|
|
continue;
|
|
}
|
|
fprintf(stderr, "QML-WARNING: %s\n", qPrintable(error.toString()));
|
|
}
|
|
fflush(stderr);
|
|
});
|
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &engine, [](const QUrl &url) {
|
|
fprintf(stderr, "QML-OBJECT-CREATION-FAILED: %s\n", qPrintable(url.toString()));
|
|
fflush(stderr);
|
|
});
|
|
|
|
engine.loadFromModule("io.github.toservetheking.Kareer", u"Main"_s);
|
|
if (engine.rootObjects().isEmpty()) {
|
|
return -1;
|
|
}
|
|
|
|
return app.exec();
|
|
}
|