Report unavailable protocols and services; send combos as real keys

Diagnostics collects what this session doesn't allow, logs it, and lists
it in the settings panel (with a dot on the gear): KWin not launching
sloposk as its keyboard (detected by WAYLAND_SOCKET, since KWin offers the
input-method global to every client), fake input missing or too old,
org.kde.keyboard unreachable, and the D-Bus control name taken.

Modifier combos go through fake input even in text fields: KWin drops
the input method's modifier mask for most clients, so Ctrl+A typed "a".

The desktop file is generated with an absolute Exec, which KWin needs to
match it: one for the installed binary, one for the build's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 11:36:56 +02:00
co-authored by Claude Opus 5.5
parent 6164839886
commit 499e5ee76e
21 changed files with 362 additions and 104 deletions
+14 -1
View File
@@ -26,6 +26,7 @@ qt_add_library(sloposk_core STATIC
window/WindowSettings.h window/WindowSettings.cpp
settings/DebouncedStore.h settings/DebouncedStore.cpp
settings/Settings.h settings/Settings.cpp
diagnostics/Diagnostics.h diagnostics/Diagnostics.cpp
)
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
@@ -39,6 +40,7 @@ set(SLOPOSK_QML_FILES
SectionHeader.qml
SettingsButton.qml
SettingsPanel.qml
ProblemList.qml
ToggleRow.qml
SliderRow.qml
Theme.qml
@@ -68,6 +70,7 @@ target_include_directories(sloposk_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/input
${CMAKE_CURRENT_SOURCE_DIR}/layout
${CMAKE_CURRENT_SOURCE_DIR}/settings
${CMAKE_CURRENT_SOURCE_DIR}/diagnostics
)
target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
@@ -77,6 +80,7 @@ qt_add_executable(sloposk
main.cpp
app/DevHooks.h app/DevHooks.cpp
app/DBusControl.h app/DBusControl.cpp
app/Checks.h app/Checks.cpp
app/QuitOnSignal.h app/QuitOnSignal.cpp
app/SystemLayouts.h app/SystemLayouts.cpp
backend/InputMethodProtocol.h
@@ -107,4 +111,13 @@ target_link_libraries(sloposk PRIVATE
)
install(TARGETS sloposk)
install(FILES ${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
# KWin grants restricted protocols by matching Exec to the absolute executable
# path. One file runs the installed binary; the other the build's, for development.
set(SLOPOSK_EXEC ${CMAKE_INSTALL_FULL_BINDIR}/sloposk)
configure_file(${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/install/org.sloposk.keyboard.desktop @ONLY)
set(SLOPOSK_EXEC ${CMAKE_CURRENT_BINARY_DIR}/sloposk)
configure_file(${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/org.sloposk.keyboard.desktop @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/install/org.sloposk.keyboard.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
+62
View File
@@ -0,0 +1,62 @@
#include "Checks.h"
#include "SystemLayouts.h"
#include "backend/KWinInputMethod.h"
#include "diagnostics/Diagnostics.h"
#include <QCoreApplication>
namespace {
QString tr(const char *text)
{
return QCoreApplication::translate("Checks", text);
}
QString fakeInputProblem(const KWinFakeInput &fakeInput)
{
const QString lost = tr("Hold Keys, shortcuts like Ctrl+A and typing outside text fields don't work.");
if (!fakeInput.isActive())
return tr("KWin doesn't let sloposk send key events (it allows the virtual keyboard it launched). ") + lost;
return tr("KWin's fake input is too old (version %1, needs %2). ")
.arg(fakeInput.boundVersion())
.arg(KWinFakeInput::RequiredVersion)
+ lost;
}
} // namespace
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin)
{
// Qt binds globals KWin already announced on construction, so the first
// check is final unless KWin restarts.
const auto checkInputMethod = [&] {
diagnostics.check(QStringLiteral("input-method"), launchedByKWin && backend.isAvailable(),
tr("Not running as KWin's virtual keyboard, so it can't type into text fields: "
"choose sloposk in System Settings → Keyboard → Virtual Keyboard."));
};
const KWinFakeInput &fakeInput = backend.fakeInput();
const auto checkFakeInput = [&] {
diagnostics.check(QStringLiteral("fake-input"), fakeInput.canSendKeysyms(), fakeInputProblem(fakeInput));
};
QObject::connect(&backend, &KWinInputMethod::availableChanged, &diagnostics, checkInputMethod);
QObject::connect(&fakeInput, &QWaylandClientExtension::activeChanged, &diagnostics, checkFakeInput);
checkInputMethod();
checkFakeInput();
}
void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts)
{
QObject::connect(&layouts, &SystemLayouts::serviceChecked, &diagnostics, [&](bool available) {
diagnostics.check(QStringLiteral("system-layouts"), available,
tr("KWin's keyboard layout service (org.kde.keyboard) isn't reachable: "
"the globe key switches only the on-screen layout."));
});
}
void reportDBusControl(Diagnostics &diagnostics, bool registered)
{
diagnostics.check(QStringLiteral("dbus-control"), registered,
tr("Couldn't claim org.sloposk.Keyboard on D-Bus (is another sloposk running?): "
"show/hide commands won't reach this one."));
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
class Diagnostics;
class KWinInputMethod;
class SystemLayouts;
// Reports to `diagnostics` what this session doesn't allow, and keeps the
// reports current as protocols and services come and go.
// KWin offers the input-method global to every client but only activates
// the process it launched, which it hands a connection: `launchedByKWin` is
// whether WAYLAND_SOCKET was set at startup.
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin);
void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts);
void reportDBusControl(Diagnostics &diagnostics, bool registered);
+10 -5
View File
@@ -27,7 +27,7 @@ SystemLayouts::SystemLayouts(QObject *parent)
bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint)));
auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &SystemLayouts::fetchCurrent);
connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { m_system = false; });
connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { setSystem(false); });
fetchCurrent();
}
@@ -38,10 +38,9 @@ void SystemLayouts::fetchCurrent()
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
const QDBusPendingReply<uint> reply = *watcher;
if (reply.isError())
return;
m_system = true;
setCurrent(reply.value());
setSystem(!reply.isError());
if (m_system)
setCurrent(reply.value());
});
}
@@ -56,6 +55,12 @@ void SystemLayouts::next(quint32 count)
setCurrent(index);
}
void SystemLayouts::setSystem(bool system)
{
m_system = system;
Q_EMIT serviceChecked(system);
}
void SystemLayouts::setCurrent(uint index)
{
if (index == m_current)
+3
View File
@@ -19,12 +19,15 @@ public:
Q_SIGNALS:
void currentChanged();
// After each attempt to reach the service, and when it goes away.
void serviceChecked(bool available);
private Q_SLOTS:
void setCurrent(uint index);
private:
void fetchCurrent();
void setSystem(bool system);
bool m_system = false;
quint32 m_current = 0;
+4 -10
View File
@@ -1,11 +1,9 @@
#include "KWinFakeInput.h"
#include <QtLogging>
#include <xkbcommon/xkbcommon.h>
namespace {
constexpr uint32_t KeysymVersion = 6; // keyboard_keysym; keyboard_key is 4
constexpr uint32_t DestroyVersion = 5;
constexpr uint32_t Released = 0;
@@ -26,15 +24,11 @@ constexpr ModifierKey ModifierKeys[] = {
} // namespace
KWinFakeInput::KWinFakeInput()
: QWaylandClientExtensionTemplate(KeysymVersion)
: QWaylandClientExtensionTemplate(RequiredVersion)
{
connect(this, &QWaylandClientExtension::activeChanged, this, [this] {
if (!isActive())
return;
if (!canSendKeysyms())
qWarning("KWin's fake input is too old (version %u, need %u): typing outside text fields is off",
boundVersion(), KeysymVersion);
authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window"));
if (isActive())
authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window"));
});
initialize();
}
@@ -52,7 +46,7 @@ uint32_t KWinFakeInput::boundVersion() const
bool KWinFakeInput::canSendKeysyms() const
{
return isActive() && boundVersion() >= KeysymVersion;
return isActive() && boundVersion() >= RequiredVersion;
}
void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
+5 -3
View File
@@ -24,10 +24,12 @@ public:
void setKeyPressed(quint32 keysym, bool pressed);
// Types each character as its keysym.
void typeText(const QString &text);
private:
// KWin offers the protocol only to clients whose desktop file lists it
// (see the .desktop.in); isActive() says whether it did.
// keyboard_keysym, which everything here uses, needs this version.
static constexpr uint32_t RequiredVersion = 6;
bool canSendKeysyms() const;
// The version KWin bound, which may be older than the one asked for:
// sending a request it lacks makes libwayland abort.
uint32_t boundVersion() const;
bool canSendKeysyms() const;
};
+3 -1
View File
@@ -70,9 +70,11 @@ void KWinInputMethod::setPreedit(const QString &text)
m_context->preedit(text);
}
// KWin drops the input method's modifier mask for most clients (it forwards
// only the key), so Ctrl+A would type "a": combos hold real modifier keys.
void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
{
if (m_context)
if (m_context && (modifiers == Qt::NoModifier || !m_fakeInput.canSendKeysyms()))
m_context->sendKeysym(keysym, modifiers);
else
m_fakeInput.sendKeysym(keysym, modifiers);
+2
View File
@@ -42,6 +42,8 @@ public:
// False when the compositor didn't hand us the input-method global.
bool isAvailable() const;
// Key events outside text fields, held keys and modifier combos.
const KWinFakeInput &fakeInput() const { return m_fakeInput; }
bool isActive() const override;
TextContext context() const override;
+18
View File
@@ -0,0 +1,18 @@
#include "Diagnostics.h"
#include <QtLogging>
void Diagnostics::check(const QString &id, bool ok, const QString &message)
{
if (ok) {
if (m_problems.remove(id) == 0)
return;
qInfo("Resolved: %s", qPrintable(id));
} else {
if (m_problems.value(id) == message)
return;
m_problems.insert(id, message);
qWarning("%s", qPrintable(message));
}
Q_EMIT problemsChanged();
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <QMap>
#include <QObject>
#include <QStringList>
#include <QtQml/qqmlregistration.h>
// Features that don't work in this session (a protocol KWin doesn't offer, a
// missing D-Bus service...), each with a sentence for the user. Logged when
// they appear or clear, and listed in the settings panel.
class Diagnostics : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created in C++")
Q_PROPERTY(QStringList problems READ problems NOTIFY problemsChanged)
public:
using QObject::QObject;
// Records the problem `id` with `message` unless `ok`; clears it if `ok`.
void check(const QString &id, bool ok, const QString &message);
QStringList problems() const { return m_problems.values(); }
Q_SIGNALS:
void problemsChanged();
private:
QMap<QString, QString> m_problems; // id → message, ordered by id
};
+9 -2
View File
@@ -1,9 +1,11 @@
#include "app/Checks.h"
#include "app/DBusControl.h"
#include "app/DevHooks.h"
#include "app/QuitOnSignal.h"
#include "app/SystemLayouts.h"
#include "backend/KWinInputMethod.h"
#include "backend/WaylandKeymap.h"
#include "diagnostics/Diagnostics.h"
#include "engine/DirectEngine.h"
#include "input/KeyboardController.h"
#include "layout/LayoutBuilder.h"
@@ -118,6 +120,8 @@ void connectTextContext(KWinInputMethod &backend, KeyboardController &controller
int main(int argc, char *argv[])
{
blockQuitSignals();
// libwayland unsets it once connected.
const bool launchedByKWin = qEnvironmentVariableIsSet("WAYLAND_SOCKET");
QGuiApplication app(argc, argv);
quitOnSignal();
QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
@@ -130,7 +134,9 @@ int main(int argc, char *argv[])
return 1;
}
Diagnostics diagnostics;
KWinInputMethod backend;
watchInputMethod(diagnostics, backend, launchedByKWin);
DirectEngine engine(backend);
LayoutModel model;
KeyboardController controller(engine, model);
@@ -142,6 +148,7 @@ int main(int argc, char *argv[])
WaylandKeymap keymap;
SystemLayouts systemLayouts;
watchSystemLayouts(diagnostics, systemLayouts);
const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model);
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
[&] { controller.setOptions(settings.typingOptions()); });
@@ -151,6 +158,7 @@ int main(int argc, char *argv[])
{QStringLiteral("layoutModel"), QVariant::fromValue(&model)},
{QStringLiteral("controller"), QVariant::fromValue(&controller)},
{QStringLiteral("settings"), QVariant::fromValue(&settings)},
{QStringLiteral("diagnostics"), QVariant::fromValue(&diagnostics)},
});
qml.loadFromModule("Sloposk", "Main");
auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first());
@@ -170,8 +178,7 @@ int main(int argc, char *argv[])
connectVisibility(visibility, backend, settings, controller, placement);
DBusControl dbus(visibility);
if (!dbus.registerOnSessionBus())
qWarning("D-Bus control unavailable (is another instance running?)");
reportDBusControl(diagnostics, dbus.registerOnSessionBus());
installDevHooks(backend, *window);
return app.exec();