summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-02 17:02:18 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:52:15 +0200
commit83669b23c371ee0bdea178fce9920585dd55704b (patch)
tree98d09b34dd5c0da4196620296757ce5e26095bcd /src
parentd217d976ccdc439803003a26252904ca8abdd520 (diff)
downloadqtmaildir-83669b23c371ee0bdea178fce9920585dd55704b.tar.gz
qtmaildir-83669b23c371ee0bdea178fce9920585dd55704b.zip
feat: add KeyMap with defaults and INI overrides
Maps key sequences to action name strings, with hardcoded vim-style defaults and QSettings-based [keys] overrides. Unknown actions and unparseable sequences are collected as warnings rather than treated as fatal, so a typo in the config cannot silently misbind or crash. Note: QKeySequence::fromString() on Qt 6.11 does not return an empty sequence for unparseable input (e.g. "NotAKey++") -- it returns a non-empty sequence whose toString() is empty. Detection uses that instead of isEmpty().
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/keymap.cpp82
-rw-r--r--src/keymap.h32
-rw-r--r--src/main_placeholder.cpp3
4 files changed, 115 insertions, 4 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e9e28e5..aeb425f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,5 +1,5 @@
add_library(qtmaildir_lib STATIC
- main_placeholder.cpp
+ keymap.cpp
)
target_include_directories(qtmaildir_lib
diff --git a/src/keymap.cpp b/src/keymap.cpp
new file mode 100644
index 0000000..6e43f1b
--- /dev/null
+++ b/src/keymap.cpp
@@ -0,0 +1,82 @@
+#include "keymap.h"
+
+#include <QSettings>
+
+QStringList KeyMap::knownActions()
+{
+ // Keep in sync with the actions MainWindow registers.
+ return {
+ QStringLiteral("next_thread"),
+ QStringLiteral("prev_thread"),
+ QStringLiteral("open_thread"),
+ QStringLiteral("archive"),
+ QStringLiteral("delete"),
+ QStringLiteral("spam"),
+ QStringLiteral("toggle_unread"),
+ QStringLiteral("flag"),
+ QStringLiteral("focus_query"),
+ QStringLiteral("toggle_html"),
+ QStringLiteral("load_remote"),
+ QStringLiteral("undo"),
+ QStringLiteral("sync"),
+ QStringLiteral("quit"),
+ };
+}
+
+void KeyMap::loadDefaults()
+{
+ const QHash<QString, QString> defaults = {
+ { QStringLiteral("j"), QStringLiteral("next_thread") },
+ { QStringLiteral("k"), QStringLiteral("prev_thread") },
+ { QStringLiteral("Return"), QStringLiteral("open_thread") },
+ { QStringLiteral("a"), QStringLiteral("archive") },
+ { QStringLiteral("d"), QStringLiteral("delete") },
+ { QStringLiteral("N"), QStringLiteral("toggle_unread") },
+ { QStringLiteral("F"), QStringLiteral("flag") },
+ { QStringLiteral("/"), QStringLiteral("focus_query") },
+ { QStringLiteral("h"), QStringLiteral("toggle_html") },
+ { QStringLiteral("u"), QStringLiteral("undo") },
+ { QStringLiteral("G"), QStringLiteral("sync") },
+ { QStringLiteral("Ctrl+Q"), QStringLiteral("quit") },
+ };
+
+ for (auto it = defaults.cbegin(); it != defaults.cend(); ++it)
+ m_bindings.insert(QKeySequence::fromString(it.key()), it.value());
+}
+
+void KeyMap::loadOverrides(QSettings &settings)
+{
+ const QStringList known = knownActions();
+
+ settings.beginGroup(QStringLiteral("keys"));
+ const QStringList keys = settings.childKeys();
+ for (const QString &key : keys) {
+ const QString action = settings.value(key).toString();
+
+ const QKeySequence sequence = QKeySequence::fromString(key);
+ // QKeySequence::fromString() does not return an empty sequence for
+ // unparseable input; it returns a non-empty sequence whose
+ // toString() is empty (verified on Qt 6.11). Use that to detect
+ // garbage input instead.
+ if (sequence.isEmpty() || sequence.toString().isEmpty()) {
+ m_warnings.append(
+ QStringLiteral("Unparseable key sequence '%1' in [keys]").arg(key));
+ continue;
+ }
+
+ if (!known.contains(action)) {
+ m_warnings.append(
+ QStringLiteral("Unknown action '%1' bound to '%2' in [keys]")
+ .arg(action, key));
+ continue;
+ }
+
+ m_bindings.insert(sequence, action);
+ }
+ settings.endGroup();
+}
+
+QString KeyMap::actionFor(const QKeySequence &sequence) const
+{
+ return m_bindings.value(sequence);
+}
diff --git a/src/keymap.h b/src/keymap.h
new file mode 100644
index 0000000..3f0f85c
--- /dev/null
+++ b/src/keymap.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <QHash>
+#include <QKeySequence>
+#include <QStringList>
+
+class QSettings;
+
+/// Maps key sequences to action names. Action names are plain strings so this
+/// class has no dependency on the widgets that implement the actions.
+class KeyMap
+{
+public:
+ /// Every action name the application understands. loadOverrides() rejects
+ /// anything not in this set, so a typo in the config cannot bind silently.
+ static QStringList knownActions();
+
+ void loadDefaults();
+
+ /// Reads the [keys] group. Invalid sequences and unknown action names are
+ /// collected into warnings() rather than throwing or aborting.
+ void loadOverrides(QSettings &settings);
+
+ /// Empty string when nothing is bound.
+ QString actionFor(const QKeySequence &sequence) const;
+
+ QStringList warnings() const { return m_warnings; }
+
+private:
+ QHash<QKeySequence, QString> m_bindings;
+ QStringList m_warnings;
+};
diff --git a/src/main_placeholder.cpp b/src/main_placeholder.cpp
deleted file mode 100644
index 9ddb3f2..0000000
--- a/src/main_placeholder.cpp
+++ /dev/null
@@ -1,3 +0,0 @@
-// Placeholder so the library target has a source file before real code lands.
-// Removed in Task 2.
-namespace { int qtmaildir_placeholder = 0; }