Add --db and let the user choose where the database lives

- `kareer --db <path>` works for the GUI and every subcommand, before
  or after the subcommand name, and creates the file if missing. main()
  strips it from argv before CLI/GUI routing; every parser declares it
  so it shows in --help.
- Path precedence: --db > KAREER_DB_PATH > the location chosen in the
  GUI (kareerrc [Database] Path, read at startup for GUI and CLI alike)
  > $XDG_DATA_HOME/kareer/kareer.sqlite.
- First GUI run (nothing forced, no file yet, or the configured file
  has gone missing): DatabaseSetupDialog offers the default location, a
  folder of the user's choice, or an existing database used in place.
  Until then JobsDatabase opens nothing.
- Preferences gains a Database section: Move to... (copies, leaves the
  original), Open Existing..., Use Default Location. Refuses foreign
  SQLite files and unwritable ones.
- Models call JobsDatabase::reopenIfPathChanged() on refresh, so
  switching databases needs no restart.
- Link KF6::ConfigCore; add a QuickDialogs2 configure-time guard; add
  kconfig to the Arch dependencies and a note to the Flatpak manifest.
- README documents --db, the precedence, and the first-run choice, and
  gains Fedora 44 build dependencies; CONTRIBUTING gets a dev recipe.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BxDf7HqD1xPnsZP8wt3NTk
This commit is contained in:
2026-09-11 15:42:00 -05:00
co-authored by Claude Opus 5
parent aa3a31f014
commit 260704cf9b
17 changed files with 790 additions and 16 deletions
+4
View File
@@ -21,6 +21,7 @@ qt_add_qml_module(kareer
qml/DashboardPage.qml
qml/SankeyDiagram.qml
qml/SettingsPage.qml
qml/DatabaseSetupDialog.qml
SOURCES
jobsmodel.cpp
jobsmodel.h
@@ -32,6 +33,8 @@ qt_add_qml_module(kareer
jobeditmodel.h
appcolorscheme.cpp
appcolorscheme.h
databaselocation.cpp
databaselocation.h
)
target_include_directories(kareer PRIVATE ${CMAKE_BINARY_DIR})
@@ -44,6 +47,7 @@ target_link_libraries(kareer PRIVATE
Qt6::Quick
Qt6::QuickControls2
Qt6::Sql
KF6::ConfigCore
KF6::I18n
KF6::I18nQml
KF6::CoreAddons
+17 -1
View File
@@ -26,6 +26,13 @@ using namespace Qt::Literals::StringLiterals;
namespace
{
/// --db is applied and stripped in main() before any parser runs; declaring
/// it here only documents it in each subcommand's --help.
void addDbOption(QCommandLineParser &parser)
{
parser.addOption({u"db"_s, u"Use this database file instead of the configured one"_s, u"path"_s});
}
QJsonValue optionalInt(int value)
{
return value < 0 ? QJsonValue() : QJsonValue(value);
@@ -159,6 +166,7 @@ int runAdd(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Add a new job application"_s);
parser.addHelpOption();
addDbOption(parser);
addCommonJobOptions(parser);
parser.process(QStringList{program} + args);
@@ -225,6 +233,7 @@ int runList(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"List job applications"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"stage"_s, u"Filter by stage"_s, u"stage"_s});
parser.addOption({u"company"_s, u"Filter by company (substring match)"_s, u"text"_s});
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
@@ -272,6 +281,7 @@ int runShow(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Show one job application"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
parser.process(QStringList{program} + args);
@@ -310,6 +320,7 @@ int runUpdate(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Update fields on an existing application"_s);
parser.addHelpOption();
addDbOption(parser);
addCommonJobOptions(parser);
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
parser.process(QStringList{program} + args);
@@ -410,6 +421,7 @@ int runStage(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Move an application to a new stage"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
parser.addPositionalArgument(u"stage"_s, u"New stage: %1"_s.arg(JobStage::canonicalStages().join(u", "_s)));
@@ -453,6 +465,7 @@ int runDelete(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Delete an application"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"yes"_s, u"Confirm deletion"_s});
parser.addPositionalArgument(u"id"_s, u"Application id"_s);
parser.process(QStringList{program} + args);
@@ -490,6 +503,7 @@ int runStats(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"Summary statistics across all applications"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
parser.process(QStringList{program} + args);
@@ -531,6 +545,7 @@ int runStages(const QString &program, const QStringList &args)
QCommandLineParser parser;
parser.setApplicationDescription(u"List the canonical pipeline stages"_s);
parser.addHelpOption();
addDbOption(parser);
parser.addOption({u"json"_s, u"Print machine-readable JSON"_s});
parser.process(QStringList{program} + args);
@@ -557,7 +572,8 @@ int runHelp()
<< u" list List job applications\n"_s << u" show Show one job application\n"_s << u" update Update fields on an existing application\n"_s
<< u" stage Move an application to a new stage\n"_s << u" delete Delete an application\n"_s << u" stats Summary statistics\n"_s
<< u" stages List the canonical pipeline stages\n\n"_s << u"Run 'kareer <command> --help' for the options of a specific command.\n"_s
<< u"Running kareer with no command (or an unrecognized one) starts the GUI.\n"_s;
<< u"Running kareer with no command (or an unrecognized one) starts the GUI.\n\n"_s << u"Global options:\n"_s
<< u" --db <path> Use this database file (created if missing) instead of the configured one\n"_s;
return 0;
}
+215
View File
@@ -0,0 +1,215 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "databaselocation.h"
#include "jobsdatabase.h"
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSqlDatabase>
using namespace Qt::Literals::StringLiterals;
namespace
{
constexpr auto ConfigFile = "kareerrc";
constexpr auto ConfigGroup = "Database";
constexpr auto ConfigKey = "Path";
constexpr auto DatabaseFileName = "kareer.sqlite";
KConfigGroup databaseGroup()
{
return KConfigGroup(KSharedConfig::openConfig(QString::fromLatin1(ConfigFile)), QString::fromLatin1(ConfigGroup));
}
QString folderFile(const QUrl &folder)
{
return QDir(folder.toLocalFile()).filePath(QString::fromLatin1(DatabaseFileName));
}
}
DatabaseLocation::DatabaseLocation(QObject *parent)
: QObject(parent)
{
if (JobsDatabase::selectionPending() && !JobsDatabase::configuredPath().isEmpty()) {
m_missingPath = JobsDatabase::configuredPath();
}
}
void DatabaseLocation::loadConfiguredPath()
{
JobsDatabase::setConfiguredPath(databaseGroup().readEntry(ConfigKey, QString()));
}
bool DatabaseLocation::needsSetup()
{
return !JobsDatabase::hasForcedPath() && !QFileInfo::exists(JobsDatabase::defaultPath());
}
QString DatabaseLocation::path() const
{
return JobsDatabase::selectionPending() ? QString() : JobsDatabase::defaultPath();
}
QString DatabaseLocation::standardPath() const
{
return JobsDatabase::standardPath();
}
bool DatabaseLocation::setupPending() const
{
return JobsDatabase::selectionPending();
}
QString DatabaseLocation::missingPath() const
{
return m_missingPath;
}
bool DatabaseLocation::overridden() const
{
return JobsDatabase::hasForcedPath();
}
QString DatabaseLocation::lastError() const
{
return m_lastError;
}
QString DatabaseLocation::lastNotice() const
{
return m_lastNotice;
}
bool DatabaseLocation::useDefault()
{
return switchTo(QString(), false);
}
bool DatabaseLocation::createIn(const QUrl &folder)
{
if (!folder.isLocalFile()) {
setMessages(i18n("Please choose a local folder."), QString());
return false;
}
return switchTo(folderFile(folder), false);
}
bool DatabaseLocation::useFile(const QUrl &file)
{
if (!file.isLocalFile()) {
setMessages(i18n("Please choose a local file."), QString());
return false;
}
return switchTo(file.toLocalFile(), true);
}
bool DatabaseLocation::moveTo(const QUrl &folder)
{
if (!folder.isLocalFile()) {
setMessages(i18n("Please choose a local folder."), QString());
return false;
}
const QString source = JobsDatabase::defaultPath();
const QString target = folderFile(folder);
if (QFileInfo(source).canonicalFilePath() == QFileInfo(target).canonicalFilePath()) {
setMessages(i18n("The database is already in that folder."), QString());
return false;
}
if (QFileInfo::exists(target)) {
setMessages(i18n("%1 already exists. Choose another folder, or open that file instead.", target), QString());
return false;
}
if (!QFile::copy(source, target)) {
setMessages(i18n("Could not copy the database to %1.", target), QString());
return false;
}
if (!switchTo(target, true)) {
QFile::remove(target);
return false;
}
setMessages(QString(), i18n("Now using %1. The previous file at %2 was left in place.", target, source));
return true;
}
void DatabaseLocation::clearMessages()
{
setMessages(QString(), QString());
}
bool DatabaseLocation::switchTo(const QString &path, bool mustExist)
{
const QString effective = path.isEmpty() ? JobsDatabase::standardPath() : path;
if (mustExist && !QFileInfo::exists(effective)) {
setMessages(i18n("%1 does not exist.", effective), QString());
return false;
}
// Refuse to add Kareer's tables to some unrelated SQLite file.
if (QFileInfo::exists(effective)) {
const QString connection = u"kareer_probe"_s;
bool foreign = false;
{
QSqlDatabase probe = QSqlDatabase::addDatabase(u"QSQLITE"_s, connection);
probe.setDatabaseName(effective);
probe.setConnectOptions(u"QSQLITE_OPEN_READONLY"_s);
if (probe.open()) {
const QStringList tables = probe.tables();
foreign = !tables.isEmpty() && !tables.contains(u"jobs"_s);
}
probe.close();
}
QSqlDatabase::removeDatabase(connection);
if (foreign) {
setMessages(i18n("%1 is not a Kareer database.", effective), QString());
return false;
}
}
{
// Opening creates the file and schema if needed; the write check
// catches files that can be read but not written (for example a
// single-file grant inside the Flatpak sandbox, where SQLite cannot
// create its journal next to the database).
JobsDatabase db(effective);
if (!db.isOpen() || !db.lastError().isEmpty() || !db.checkWritable()) {
setMessages(i18n("Could not use %1: %2", effective, db.lastError()), QString());
return false;
}
}
KConfigGroup group = databaseGroup();
if (path.isEmpty()) {
group.deleteEntry(ConfigKey);
} else {
group.writeEntry(ConfigKey, path);
}
group.sync();
JobsDatabase::setConfiguredPath(path);
JobsDatabase::setSelectionPending(false);
m_missingPath.clear();
setMessages(QString(), QString());
Q_EMIT changed();
return true;
}
void DatabaseLocation::setMessages(const QString &error, const QString &notice)
{
if (m_lastError == error && m_lastNotice == notice) {
return;
}
m_lastError = error;
m_lastNotice = notice;
Q_EMIT messageChanged();
}
+83
View File
@@ -0,0 +1,83 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
/**
* Where the database lives, as chosen by the user: the first-run dialog and
* the Preferences page drive this. The choice is stored in kareerrc
* ([Database] Path) and pushed into JobsDatabase::setConfiguredPath(); the
* --db option and KAREER_DB_PATH still win over it (see JobsDatabase::defaultPath()).
*
* Emits changed() after every switch; Main.qml reacts by refreshing the
* models, which reopen themselves via JobsDatabase::reopenIfPathChanged().
*/
class DatabaseLocation : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/// The effective database file, or empty while the first-run choice is pending.
Q_PROPERTY(QString path READ path NOTIFY changed)
/// $XDG_DATA_HOME/kareer/kareer.sqlite.
Q_PROPERTY(QString standardPath READ standardPath CONSTANT)
Q_PROPERTY(bool setupPending READ setupPending NOTIFY changed)
/// The configured file that could not be found (so setup is being asked again), if any.
Q_PROPERTY(QString missingPath READ missingPath NOTIFY changed)
/// True when --db or KAREER_DB_PATH is in effect; the stored location is then ignored.
Q_PROPERTY(bool overridden READ overridden CONSTANT)
Q_PROPERTY(QString lastError READ lastError NOTIFY messageChanged)
Q_PROPERTY(QString lastNotice READ lastNotice NOTIFY messageChanged)
public:
explicit DatabaseLocation(QObject *parent = nullptr);
/// Reads kareerrc into JobsDatabase::setConfiguredPath(). Call once at
/// startup, for both the GUI and the CLI, so they share one database.
static void loadConfiguredPath();
/// True when the GUI should ask where the database lives: nothing forces
/// a path and the file it would open does not exist yet.
static bool needsSetup();
QString path() const;
QString standardPath() const;
bool setupPending() const;
QString missingPath() const;
bool overridden() const;
QString lastError() const;
QString lastNotice() const;
/// Create (or reuse) the database at the standard location.
Q_INVOKABLE bool useDefault();
/// Create (or reuse) kareer.sqlite inside the chosen folder.
Q_INVOKABLE bool createIn(const QUrl &folder);
/// Use an existing database file where it is.
Q_INVOKABLE bool useFile(const QUrl &file);
/// Copy the current database into the chosen folder and switch to the
/// copy. The original file is left in place.
Q_INVOKABLE bool moveTo(const QUrl &folder);
Q_INVOKABLE void clearMessages();
Q_SIGNALS:
void changed();
void messageChanged();
private:
/// Validates path, stores it (empty = standard location), and switches to it.
bool switchTo(const QString &path, bool mustExist);
void setMessages(const QString &error, const QString &notice);
QString m_missingPath;
QString m_lastError;
QString m_lastNotice;
};
+112 -6
View File
@@ -22,6 +22,10 @@ namespace
{
QAtomicInteger<int> s_connectionCounter{0};
QString s_pathOverride;
QString s_configuredPath;
bool s_selectionPending = false;
QVariant salaryToVariant(int value)
{
if (value < 0) {
@@ -38,6 +42,10 @@ int salaryFromVariant(const QVariant &value)
JobsDatabase::JobsDatabase()
{
if (s_selectionPending) {
m_lastError = u"No database selected"_s;
return;
}
init(defaultPath());
}
@@ -48,6 +56,14 @@ JobsDatabase::JobsDatabase(const QString &path)
JobsDatabase::~JobsDatabase()
{
close();
}
void JobsDatabase::close()
{
if (m_connectionName.isEmpty()) {
return;
}
{
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
if (db.isValid()) {
@@ -55,22 +71,103 @@ JobsDatabase::~JobsDatabase()
}
}
QSqlDatabase::removeDatabase(m_connectionName);
m_connectionName.clear();
m_path.clear();
}
QString JobsDatabase::defaultPath()
{
const QString overridePath = qEnvironmentVariable("KAREER_DB_PATH");
if (!overridePath.isEmpty()) {
return overridePath;
if (!s_pathOverride.isEmpty()) {
return s_pathOverride;
}
const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/kareer"_s;
QDir().mkpath(dir);
return dir + u"/kareer.sqlite"_s;
const QString envPath = qEnvironmentVariable("KAREER_DB_PATH");
if (!envPath.isEmpty()) {
return envPath;
}
if (!s_configuredPath.isEmpty()) {
return s_configuredPath;
}
return standardPath();
}
QString JobsDatabase::standardPath()
{
return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/kareer/kareer.sqlite"_s;
}
void JobsDatabase::setPathOverride(const QString &path)
{
s_pathOverride = path;
}
void JobsDatabase::setConfiguredPath(const QString &path)
{
s_configuredPath = path;
}
QString JobsDatabase::configuredPath()
{
return s_configuredPath;
}
bool JobsDatabase::hasForcedPath()
{
return !s_pathOverride.isEmpty() || !qEnvironmentVariableIsEmpty("KAREER_DB_PATH");
}
void JobsDatabase::setSelectionPending(bool pending)
{
s_selectionPending = pending;
}
bool JobsDatabase::selectionPending()
{
return s_selectionPending;
}
QString JobsDatabase::path() const
{
return m_path;
}
bool JobsDatabase::reopenIfPathChanged()
{
if (s_selectionPending) {
return false;
}
const QString wanted = defaultPath();
if (!m_path.isEmpty() && wanted == m_path) {
return false;
}
close();
m_lastError.clear();
init(wanted);
return true;
}
bool JobsDatabase::checkWritable()
{
if (!isOpen()) {
return false;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
if (!query.exec(u"PRAGMA user_version"_s) || !query.next()) {
m_lastError = query.lastError().text();
return false;
}
const int version = query.value(0).toInt();
query.finish();
if (!query.exec(u"PRAGMA user_version = %1"_s.arg(version))) {
m_lastError = query.lastError().text();
return false;
}
return true;
}
void JobsDatabase::init(const QString &path)
{
m_connectionName = u"kareer_conn_%1"_s.arg(s_connectionCounter.fetchAndAddRelaxed(1));
m_path = path;
QDir().mkpath(QFileInfo(path).absolutePath());
@@ -191,6 +288,9 @@ Job JobsDatabase::jobFromQuery(QSqlQuery &query) const
QList<Job> JobsDatabase::allJobs() const
{
QList<Job> jobs;
if (!isOpen()) {
return jobs;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(u"SELECT * FROM jobs ORDER BY date_applied DESC, id DESC"_s);
if (!query.exec()) {
@@ -204,6 +304,9 @@ QList<Job> JobsDatabase::allJobs() const
std::optional<Job> JobsDatabase::jobById(int id) const
{
if (!isOpen()) {
return std::nullopt;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(u"SELECT * FROM jobs WHERE id = :id"_s);
query.bindValue(u":id"_s, id);
@@ -379,6 +482,9 @@ bool JobsDatabase::deleteJob(int id)
QList<StageTransition> JobsDatabase::stageTransitions() const
{
QList<StageTransition> transitions;
if (!isOpen()) {
return transitions;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(u"SELECT job_id, from_stage, to_stage, changed_at FROM stage_history ORDER BY changed_at ASC, id ASC"_s);
if (!query.exec()) {
+36 -2
View File
@@ -31,10 +31,42 @@ public:
JobsDatabase(const JobsDatabase &) = delete;
JobsDatabase &operator=(const JobsDatabase &) = delete;
/// Default location: $XDG_DATA_HOME/kareer/kareer.sqlite, overridable
/// with the KAREER_DB_PATH environment variable (used by autotests).
/// The database file a default-constructed instance opens. Resolved in
/// order: the --db override, the KAREER_DB_PATH environment variable
/// (used by autotests), the path configured in kareerrc, and finally
/// $XDG_DATA_HOME/kareer/kareer.sqlite.
static QString defaultPath();
/// $XDG_DATA_HOME/kareer/kareer.sqlite, ignoring every override.
static QString standardPath();
/// Set from the --db command-line option; wins over everything else.
static void setPathOverride(const QString &path);
/// The user's chosen location (kareerrc); empty means standardPath().
static void setConfiguredPath(const QString &path);
static QString configuredPath();
/// True when --db or KAREER_DB_PATH decides the path, so the configured
/// location has no effect.
static bool hasForcedPath();
/// While true (GUI first run, before the user has picked a location), a
/// default-constructed instance opens nothing and reports an error.
static void setSelectionPending(bool pending);
static bool selectionPending();
/// The file this instance opened, or empty if it opened nothing.
QString path() const;
/// Reopens against defaultPath() if that no longer matches path() (the
/// user picked another location). Returns true if it reopened.
bool reopenIfPathChanged();
/// Performs a harmless write (rewrites the header's user_version), so
/// callers can tell a read-only file apart from a usable one.
bool checkWritable();
bool isOpen() const;
QString lastError() const;
@@ -59,9 +91,11 @@ public:
private:
void init(const QString &path);
void close();
bool migrate();
Job jobFromQuery(class QSqlQuery &query) const;
QString m_connectionName;
QString m_path;
QString m_lastError;
};
+1
View File
@@ -17,6 +17,7 @@ JobsModel::JobsModel(QObject *parent)
void JobsModel::refresh()
{
m_db.reopenIfPathChanged();
beginResetModel();
m_jobs = m_db.allJobs();
endResetModel();
+48
View File
@@ -7,6 +7,8 @@
#include "kareer-version.h"
#include "clicommands.h"
#include "databaselocation.h"
#include "jobsdatabase.h"
#include <KAboutData>
#include <KCrash>
@@ -17,6 +19,7 @@
#include <QApplication>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QFileInfo>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQuickStyle>
@@ -63,12 +66,50 @@ static void messageHandler(QtMsgType type, const QMessageLogContext &context, co
}
}
/// 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();
return Cli::run(app);
}
@@ -99,9 +140,16 @@ int main(int argc, char *argv[])
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());
QQmlApplicationEngine engine;
KLocalization::setupLocalizedContext(&engine);
+104
View File
@@ -0,0 +1,104 @@
/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
pragma ComponentBehavior: Bound
// First-run choice of where the database lives. Shown by Main.qml while
// DatabaseLocation.setupPending is true; it cannot be dismissed until a
// location has been picked, since nothing can be saved before then.
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Dialogs
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import io.github.toservetheking.Kareer
Kirigami.Dialog {
id: root
title: i18nc("@title:window", "Welcome to Kareer")
showCloseButton: false
closePolicy: QQC2.Popup.NoAutoClose
modal: true
preferredWidth: Kirigami.Units.gridUnit * 26
standardButtons: Kirigami.Dialog.NoButton
onOpened: DatabaseLocation.clearMessages()
ColumnLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
wrapMode: Text.Wrap
text: DatabaseLocation.missingPath.length > 0
? xi18nc("@info", "The database at <filename>%1</filename> could not be found. Choose where Kareer should keep your applications.", DatabaseLocation.missingPath)
: i18nc("@info", "Choose where Kareer should keep your applications.")
}
Kirigami.InlineMessage {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
type: Kirigami.MessageType.Error
text: DatabaseLocation.lastError
visible: text.length > 0
}
FormCard.FormButtonDelegate {
Layout.fillWidth: true
icon.name: "document-new"
text: i18nc("@action:button", "Create in the default location")
description: DatabaseLocation.standardPath
onClicked: {
if (DatabaseLocation.useDefault()) {
root.close();
}
}
}
FormCard.FormButtonDelegate {
Layout.fillWidth: true
icon.name: "folder-new"
text: i18nc("@action:button", "Choose a folder…")
description: i18nc("@info", "Create a new database in a folder of your choice, for example a synced folder.")
onClicked: folderDialog.open()
}
FormCard.FormButtonDelegate {
Layout.fillWidth: true
icon.name: "document-open"
text: i18nc("@action:button", "Open an existing database…")
description: i18nc("@info", "Use a kareer.sqlite file you already have, where it is.")
onClicked: fileDialog.open()
}
}
FolderDialog {
id: folderDialog
title: i18nc("@title:window", "Choose a Folder for the Database")
onAccepted: {
if (DatabaseLocation.createIn(selectedFolder)) {
root.close();
}
}
}
FileDialog {
id: fileDialog
title: i18nc("@title:window", "Open Database")
fileMode: FileDialog.OpenFile
nameFilters: [i18nc("@item:inlistbox", "Kareer databases (*.sqlite)"), i18nc("@item:inlistbox", "All files (*)")]
onAccepted: {
if (DatabaseLocation.useFile(selectedFile)) {
root.close();
}
}
}
}
+21
View File
@@ -25,6 +25,27 @@ Kirigami.ApplicationWindow {
id: jobsModel
}
// The database moved (first-run choice or Preferences): reload the list,
// which cascades to the dashboard via countChanged, and leave any open
// edit form, since its job id belonged to the previous database.
Connections {
target: DatabaseLocation
function onChanged(): void {
jobsModel.refresh();
root.showDashboard();
}
}
DatabaseSetupDialog {
id: setupDialog
}
Component.onCompleted: {
if (DatabaseLocation.setupPending) {
setupDialog.open();
}
}
// The job currently open in the edit form, so the sidebar can keep it highlighted.
property int currentJobId: -1
+90
View File
@@ -11,6 +11,7 @@ pragma ComponentBehavior: Bound
// header, no card/border around them.
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Dialogs
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import io.github.toservetheking.Kareer
@@ -75,5 +76,94 @@ Kirigami.ScrollablePage {
displayText: AppColorScheme.activeColorSchemeName
}
}
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: i18nc("@title:group", "Database")
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: i18nc("@label", "Location")
}
QQC2.Label {
Layout.fillWidth: true
text: DatabaseLocation.path
wrapMode: Text.WrapAnywhere
opacity: 0.7
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Information
text: i18nc("@info", "Set by the --db option or the KAREER_DB_PATH environment variable, so it can't be changed here.")
visible: DatabaseLocation.overridden
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Error
text: DatabaseLocation.lastError
visible: text.length > 0
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Positive
text: DatabaseLocation.lastNotice
visible: text.length > 0
}
Flow {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
enabled: !DatabaseLocation.overridden
QQC2.Button {
icon.name: "folder-move"
text: i18nc("@action:button", "Move to…")
QQC2.ToolTip.text: i18nc("@info:tooltip", "Copy the database into another folder and use the copy from now on. The current file is left in place.")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: moveFolderDialog.open()
}
QQC2.Button {
icon.name: "document-open"
text: i18nc("@action:button", "Open Existing…")
onClicked: openFileDialog.open()
}
QQC2.Button {
icon.name: "edit-reset"
text: i18nc("@action:button", "Use Default Location")
visible: DatabaseLocation.path !== DatabaseLocation.standardPath
QQC2.ToolTip.text: xi18nc("@info:tooltip", "Switch to <filename>%1</filename>. Nothing is copied.", DatabaseLocation.standardPath)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: DatabaseLocation.useDefault()
}
}
}
}
Component.onCompleted: DatabaseLocation.clearMessages()
FolderDialog {
id: moveFolderDialog
title: i18nc("@title:window", "Move Database To")
onAccepted: DatabaseLocation.moveTo(selectedFolder)
}
FileDialog {
id: openFileDialog
title: i18nc("@title:window", "Open Database")
fileMode: FileDialog.OpenFile
nameFilters: [i18nc("@item:inlistbox", "Kareer databases (*.sqlite)"), i18nc("@item:inlistbox", "All files (*)")]
onAccepted: DatabaseLocation.useFile(selectedFile)
}
}
+1
View File
@@ -17,6 +17,7 @@ StatsModel::StatsModel(QObject *parent)
void StatsModel::refresh()
{
m_db.reopenIfPathChanged();
m_jobs = m_db.allJobs();
Q_EMIT changed();
}