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:
@@ -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>
|
||||
Reference in New Issue
Block a user