Advance redbear-full Wayland, greeter, and Qt integration

Consolidate the active desktop path around redbear-full while landing the greeter/session stack and the runtime fixes needed to keep Wayland and KWin bring-up moving forward.
This commit is contained in:
2026-04-19 17:59:58 +01:00
parent 370d27f44d
commit 9880e0a5b2
137 changed files with 14176 additions and 2016 deletions
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.20)
project(redbear-greeter-ui LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick QuickControls2)
qt_add_executable(redbear-greeter-ui
main.cpp
greeter_backend.cpp
greeter_backend.h
resources.qrc
)
target_compile_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
target_link_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
target_link_libraries(redbear-greeter-ui PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Qml
Qt6::Quick
Qt6::QuickControls2
)
install(TARGETS redbear-greeter-ui RUNTIME DESTINATION bin)
@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: root
visible: true
visibility: Window.FullScreen
color: "#11090a"
title: "Red Bear Greeter"
function submitLogin() {
greeterBackend.submitLogin(usernameField.text, passwordField.text)
}
Rectangle {
anchors.fill: parent
color: "#11090a"
Image {
anchors.fill: parent
source: greeterBackend.backgroundUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
opacity: 0.88
}
Rectangle {
anchors.fill: parent
color: "#230a0d"
opacity: 0.45
}
}
Pane {
width: Math.min(parent.width * 0.42, 620)
anchors.centerIn: parent
padding: 28
background: Rectangle {
radius: 18
color: "#cc150c0f"
border.color: "#66f7d7d7"
border.width: 1
}
ColumnLayout {
anchors.fill: parent
spacing: 18
Item {
Layout.fillWidth: true
Layout.preferredHeight: 156
Image {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
source: greeterBackend.iconUrl
width: 108
height: 108
fillMode: Image.PreserveAspectFit
asynchronous: true
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: 4
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: "Red Bear OS"
font.pixelSize: 26
font.bold: true
color: "#fff4f4"
}
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: greeterBackend.sessionName
font.pixelSize: 15
color: "#f1c5c5"
}
}
}
TextField {
id: usernameField
Layout.fillWidth: true
placeholderText: "Username"
enabled: !greeterBackend.busy
selectByMouse: true
color: "#fff8f8"
font.pixelSize: 18
onAccepted: passwordField.forceActiveFocus()
}
TextField {
id: passwordField
Layout.fillWidth: true
placeholderText: "Password"
enabled: !greeterBackend.busy
selectByMouse: true
echoMode: TextInput.Password
color: "#fff8f8"
font.pixelSize: 18
onAccepted: root.submitLogin()
}
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: greeterBackend.message
color: greeterBackend.state === "fatal_error" ? "#ffb4b4" : "#ffe7e7"
font.pixelSize: 15
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: greeterBackend.busy
visible: running
}
RowLayout {
Layout.fillWidth: true
spacing: 12
Button {
Layout.fillWidth: true
text: greeterBackend.busy ? "Working…" : "Log In"
enabled: !greeterBackend.busy
onClicked: root.submitLogin()
}
Button {
text: "Shutdown"
enabled: !greeterBackend.busy
onClicked: greeterBackend.requestShutdown()
}
Button {
text: "Reboot"
enabled: !greeterBackend.busy
onClicked: greeterBackend.requestReboot()
}
}
}
}
Component.onCompleted: usernameField.forceActiveFocus()
}
@@ -0,0 +1,296 @@
#include "greeter_backend.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTimer>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <cstddef>
#include <cstring>
namespace {
constexpr auto kGreeterSocketPath = "/run/redbear-greeterd.sock";
constexpr auto kConnectTimeoutMs = 1500;
constexpr auto kReadTimeoutMs = 5000;
bool waitForReadable(int fd, int timeoutMs, QString *error) {
pollfd descriptor{};
descriptor.fd = fd;
descriptor.events = POLLIN;
const auto pollResult = ::poll(&descriptor, 1, timeoutMs);
if (pollResult > 0) {
return true;
}
if (pollResult == 0) {
*error = QStringLiteral("timed out waiting for greeter response");
return false;
}
*error = QStringLiteral("failed while waiting for greeter response: %1").arg(QString::fromLocal8Bit(std::strerror(errno)));
return false;
}
}
GreeterBackend::GreeterBackend(QObject *parent) : QObject(parent) {}
QUrl GreeterBackend::backgroundUrl() const {
return m_backgroundUrl;
}
QUrl GreeterBackend::iconUrl() const {
return m_iconUrl;
}
QString GreeterBackend::sessionName() const {
return m_sessionName;
}
QString GreeterBackend::state() const {
return m_state;
}
QString GreeterBackend::message() const {
return m_message;
}
bool GreeterBackend::busy() const {
return m_busy;
}
void GreeterBackend::initialize() {
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("hello")},
{QStringLiteral("version"), 1}})
.toJson(QJsonDocument::Compact));
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type != QStringLiteral("hello_ok")) {
applyError(response.message.isEmpty() ? QStringLiteral("unexpected greeter hello response") : response.message);
return;
}
setGreeting(response.backgroundPath, response.iconPath, response.sessionName);
setStatus(response.state, response.message);
}
void GreeterBackend::submitLogin(const QString &username, const QString &password) {
if (m_busy) {
return;
}
if (username.trimmed().isEmpty() || password.isEmpty()) {
setStatus(QStringLiteral("greeter_ready"), QStringLiteral("Enter both username and password."));
return;
}
setBusy(true);
setStatus(QStringLiteral("authenticating"), QStringLiteral("Authenticating"));
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("submit_login")},
{QStringLiteral("username"), username},
{QStringLiteral("password"), password}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("login_result")) {
setStatus(response.state, response.message);
if (response.ok) {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
}
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected login response") : response.message);
}
void GreeterBackend::requestShutdown() {
if (m_busy) {
return;
}
setBusy(true);
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting shutdown"));
const auto response = sendRequest(
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_shutdown")}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("action_result")) {
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected shutdown response") : response.message);
}
void GreeterBackend::requestReboot() {
if (m_busy) {
return;
}
setBusy(true);
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting reboot"));
const auto response = sendRequest(
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_reboot")}})
.toJson(QJsonDocument::Compact));
setBusy(false);
if (!response.transportOk) {
applyError(response.transportError);
return;
}
if (response.type == QStringLiteral("action_result")) {
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
return;
}
applyError(response.message.isEmpty() ? QStringLiteral("unexpected reboot response") : response.message);
}
GreeterBackend::Response GreeterBackend::sendRequest(const QByteArray &payload) const {
Response response;
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) {
response.transportError = QStringLiteral("failed to create greeter socket: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
return response;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::strncpy(address.sun_path, kGreeterSocketPath, sizeof(address.sun_path) - 1);
const auto addressSize = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + std::strlen(address.sun_path) + 1);
if (::connect(fd, reinterpret_cast<sockaddr *>(&address), addressSize) != 0) {
response.transportError = QStringLiteral("failed to connect to %1: %2")
.arg(QString::fromLatin1(kGreeterSocketPath),
QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
const auto fullPayload = payload + '\n';
qsizetype written = 0;
while (written < fullPayload.size()) {
const auto chunk = ::write(fd, fullPayload.constData() + written, static_cast<size_t>(fullPayload.size() - written));
if (chunk < 0) {
response.transportError = QStringLiteral("failed to write greeter request: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
written += chunk;
}
QString waitError;
if (!waitForReadable(fd, kReadTimeoutMs, &waitError)) {
response.transportError = waitError;
::close(fd);
return response;
}
QByteArray reply;
char buffer[1024];
while (reply.indexOf('\n') < 0) {
const auto chunk = ::read(fd, buffer, sizeof(buffer));
if (chunk < 0) {
response.transportError = QStringLiteral("failed to read greeter response: %1")
.arg(QString::fromLocal8Bit(std::strerror(errno)));
::close(fd);
return response;
}
if (chunk == 0) {
break;
}
reply.append(buffer, static_cast<int>(chunk));
if (reply.indexOf('\n') < 0 && !waitForReadable(fd, kConnectTimeoutMs, &waitError)) {
response.transportError = waitError;
::close(fd);
return response;
}
}
::close(fd);
const auto newlineIndex = reply.indexOf('\n');
if (newlineIndex >= 0) {
reply.truncate(newlineIndex);
}
const auto document = QJsonDocument::fromJson(reply);
if (!document.isObject()) {
response.transportError = QStringLiteral("invalid greeter response payload");
return response;
}
const auto object = document.object();
response.transportOk = true;
response.type = object.value(QStringLiteral("type")).toString();
response.ok = object.value(QStringLiteral("ok")).toBool();
response.state = object.value(QStringLiteral("state")).toString();
response.message = object.value(QStringLiteral("message")).toString();
response.sessionName = object.value(QStringLiteral("session_name")).toString();
response.backgroundPath = object.value(QStringLiteral("background")).toString();
response.iconPath = object.value(QStringLiteral("icon")).toString();
if (response.type == QStringLiteral("error") && response.message.isEmpty()) {
response.message = QStringLiteral("greeter returned an unspecified error");
}
return response;
}
void GreeterBackend::setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName) {
const auto nextBackground = backgroundPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(backgroundPath);
const auto nextIcon = iconPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(iconPath);
const auto nextSessionName = sessionName.isEmpty() ? QStringLiteral("KDE on Wayland") : sessionName;
if (m_backgroundUrl == nextBackground && m_iconUrl == nextIcon && m_sessionName == nextSessionName) {
return;
}
m_backgroundUrl = nextBackground;
m_iconUrl = nextIcon;
m_sessionName = nextSessionName;
emit greetingChanged();
}
void GreeterBackend::setStatus(const QString &state, const QString &message) {
const auto nextState = state.isEmpty() ? QStringLiteral("greeter_ready") : state;
if (m_state == nextState && m_message == message) {
return;
}
m_state = nextState;
m_message = message;
emit statusChanged();
}
void GreeterBackend::setBusy(bool busy) {
if (m_busy == busy) {
return;
}
m_busy = busy;
emit busyChanged();
}
void GreeterBackend::applyError(const QString &message) {
setStatus(QStringLiteral("fatal_error"), message);
}
@@ -0,0 +1,60 @@
#pragma once
#include <QObject>
#include <QUrl>
class GreeterBackend final : public QObject {
Q_OBJECT
Q_PROPERTY(QUrl backgroundUrl READ backgroundUrl NOTIFY greetingChanged)
Q_PROPERTY(QUrl iconUrl READ iconUrl NOTIFY greetingChanged)
Q_PROPERTY(QString sessionName READ sessionName NOTIFY greetingChanged)
Q_PROPERTY(QString state READ state NOTIFY statusChanged)
Q_PROPERTY(QString message READ message NOTIFY statusChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit GreeterBackend(QObject *parent = nullptr);
[[nodiscard]] QUrl backgroundUrl() const;
[[nodiscard]] QUrl iconUrl() const;
[[nodiscard]] QString sessionName() const;
[[nodiscard]] QString state() const;
[[nodiscard]] QString message() const;
[[nodiscard]] bool busy() const;
Q_INVOKABLE void initialize();
Q_INVOKABLE void submitLogin(const QString &username, const QString &password);
Q_INVOKABLE void requestShutdown();
Q_INVOKABLE void requestReboot();
signals:
void greetingChanged();
void statusChanged();
void busyChanged();
private:
struct Response {
bool transportOk = false;
QString transportError;
QString type;
bool ok = false;
QString state;
QString message;
QString sessionName;
QString backgroundPath;
QString iconPath;
};
[[nodiscard]] Response sendRequest(const QByteArray &payload) const;
void setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName);
void setStatus(const QString &state, const QString &message);
void setBusy(bool busy);
void applyError(const QString &message);
QUrl m_backgroundUrl;
QUrl m_iconUrl;
QString m_sessionName = QStringLiteral("KDE on Wayland");
QString m_state = QStringLiteral("starting");
QString m_message = QStringLiteral("Connecting to greeter");
bool m_busy = false;
};
@@ -0,0 +1,25 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include "greeter_backend.h"
int main(int argc, char *argv[]) {
qputenv("QT_QUICK_CONTROLS_STYLE", QByteArrayLiteral("Basic"));
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("Basic"));
GreeterBackend backend;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("greeterBackend"), &backend);
engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
if (engine.rootObjects().isEmpty()) {
return 1;
}
backend.initialize();
return app.exec();
}
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>Main.qml</file>
</qresource>
</RCC>