From ccd88b7fa33fc27e3d71b57f59dea55ed7430d6e Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 5 Aug 2021 10:51:39 +0200 Subject: Add shibboleth login via browser --- CMakeLists.txt | 3 +- src/loginform.cpp | 168 ++++++++++++++++++++++++++++++++++++------- src/loginform.h | 17 ++++- src/loginform.ui | 204 ++++++++++++++++++++++++++++++++++++++++++----------- src/mainwindow.cpp | 11 ++- src/mainwindow.h | 1 + src/settings.h | 6 +- src/webview.cpp | 109 ++++++++++++++++++++++++++++ src/webview.h | 44 ++++++++++++ src/x11util.h | 2 +- 10 files changed, 491 insertions(+), 74 deletions(-) create mode 100644 src/webview.cpp create mode 100644 src/webview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 28d953f..3b6b7be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ set(CMAKE_AUTOMOC ON) file(GLOB SRCS src/*.cpp src/*.c) file(GLOB UIS src/*.ui) -FIND_PACKAGE(Qt5 COMPONENTS Widgets X11Extras Svg Network REQUIRED) +FIND_PACKAGE(Qt5 COMPONENTS Widgets X11Extras Svg Network WebKitWidgets REQUIRED) FIND_PACKAGE(X11 REQUIRED) IF(X11_Xscreensaver_FOUND) @@ -56,6 +56,7 @@ target_link_libraries ( qt-lightdm-greeter Qt5::X11Extras Qt5::Svg Qt5::Network + Qt5::WebKitWidgets ${LIGHTDM_QT_LIBRARIES} ${X11_LIBRARIES} ${X11_Xscreensaver_LIB} diff --git a/src/loginform.cpp b/src/loginform.cpp index a53c7ab..eecf34b 100644 --- a/src/loginform.cpp +++ b/src/loginform.cpp @@ -8,13 +8,15 @@ * Please refer to the LICENSE file for a copy of the license. */ +#include + +#include "x11util.h" #include "loginform.h" #include "ui_loginform.h" #include "settings.h" #include "global.h" #include "namereplace.h" #include "loginrpc.h" -#include "x11util.h" #undef KeyPress #undef KeyRelease #undef FocusIn @@ -29,6 +31,9 @@ #include #include #include +#include "webview.h" +#include + #include void createSimpleBackground(); @@ -36,10 +41,19 @@ void createSimpleBackground(); LoginForm::LoginForm(QWidget *parent) : QWidget(parent), ui(new Ui::LoginForm), + browser(nullptr), clearMsg(false), capsOn(-1) { ui->setupUi(this); + origSize = shibSize = sizeHint(); + if (this->parentWidget() != nullptr) { + shibSize.setWidth(qMin(1000, int(this->parentWidget()->width() * .75f) )); + shibSize.setHeight(qMin(700, int(this->parentWidget()->height() * .75f) )); + } else { + shibSize.rwidth() += 350; + shibSize.rheight() += 250; + } initialize(); int port = Settings::rpcPort(); if (port != 0) { @@ -49,14 +63,14 @@ LoginForm::LoginForm(QWidget *parent) : return; ui->userInput->setText(username); ui->passwordInput->setText(password); - this->startAuthentication(); + this->startFormBasedAuthentication(); }); } + connect(ui->loginChooser, &QStackedWidget::currentChanged, this, &LoginForm::setBrowserSize); } LoginForm::~LoginForm() { - delete ui; } @@ -69,14 +83,45 @@ void LoginForm::setFocus(Qt::FocusReason reason) } } +void LoginForm::resizeEvent(QResizeEvent *e) +{ + if (this->parentWidget() != nullptr) { + shibSize.setWidth(qMin(1000, int(this->parentWidget()->width() * .75f) )); + shibSize.setHeight(qMin(700, int(this->parentWidget()->height() * .75f) )); + } + const QSize *size = nullptr; + if (ui->loginChooser->currentWidget() == ui->shibPage) { + size = &shibSize; + } else { + size = &origSize; + } + if (*size != e->size()) { + e->ignore(); + setMinimumSize(*size); + setFixedSize(*size); + setBaseSize(*size); + int pw = 0, ph = 0; + if (this->parentWidget() != nullptr) { + this->parentWidget()->pos(); + pw = (this->parentWidget()->width() - size->width()) / 2; + ph = (this->parentWidget()->height() - size->height()) / 2; + } + setGeometry(pw, pw, size->width(), size->height()); + emit resized(); + setBrowserSize(); + return; + } + QWidget::resizeEvent(e); +} void LoginForm::initialize() { QString path = Settings::miniIconFile(); QPixmap pixmap; if (!path.isEmpty()) { + // Try to get the default size, in case this is an SVG QSize size = QSvgRenderer(path).defaultSize(); - if (!size.isValid()) { + if (!size.isValid()) { // if not, use maximum of destination size = ui->iconLabel->maximumSize(); } else { size = size.boundedTo(ui->iconLabel->maximumSize()).expandedTo(ui->iconLabel->minimumSize()); @@ -132,13 +177,38 @@ void LoginForm::initialize() if (!Settings::guestSessionStartButtonText().isEmpty()) { ui->guestStartButton->setText(Settings::guestSessionStartButtonText()); } - connect(ui->loginButton, &QAbstractButton::released, this, [this]() { - ui->loginChooser->setCurrentWidget(ui->loginPage); - ui->userInput->setFocus(); - }); connect(ui->guestButton, &QAbstractButton::released, this, [this]() { ui->loginChooser->setCurrentWidget(ui->guestPage); }); + } else { + ui->guestButton->hide(); + } + + if (Settings::shibSessionEnabled()) { + connect(ui->shibButton, &QAbstractButton::released, this, [this]() { + if (browser == nullptr) { + browser = new WebView(ui->shibPage); + ui->verticalLayout_5->addWidget(browser); + connect(browser, &WebView::triggerReset, [this](const QString &message) { + this->showMessage(message, true); + ui->loginChooser->setCurrentWidget(ui->welcomePage); + }); + connect(browser, &WebView::startAuthentication, this, &LoginForm::startAuthAs); + } + browser->reset(Settings::shibUrl()); + ui->loginChooser->setCurrentWidget(ui->shibPage); + }); + // Reduce minimum size of hostname/icon bar + ui->frame->setMinimumSize(10, 30); + ui->frame->setMaximumSize(99999, ui->iconLabel->height()); + ui->frame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + ui->loginChooser->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + } else { + ui->shibButton->hide(); + } + + if (Settings::guestSessionEnabled() || Settings::shibSessionEnabled()) { + ui->loginChooser->setCurrentWidget(ui->welcomePage); connect(ui->backButton, &QAbstractButton::released, this, [this]() { resetForm(); }); @@ -149,7 +219,10 @@ void LoginForm::initialize() ui->backButton->show(); } }); - ui->loginChooser->setCurrentWidget(ui->welcomePage); + connect(ui->loginButton, &QAbstractButton::released, this, [this]() { + ui->loginChooser->setCurrentWidget(ui->loginPage); + ui->userInput->setFocus(); + }); } else { ui->loginChooser->setCurrentWidget(ui->loginPage); } @@ -208,19 +281,12 @@ void LoginForm::checkCaps() } } -void LoginForm::startAuthentication() +void LoginForm::startFormBasedAuthentication() { QString username(ui->userInput->text().trimmed()); NameReplace::replace(username); std::cerr << "Logging in as " << username.toStdString() << std::endl; - if (Global::testMode()) { - showMessage(QLatin1String("Test mode..."), true); - return; - } - if (Global::greeter()->inAuthentication()) { - Global::greeter()->cancelAuthentication(); - } if (ui->userInput->text().isEmpty()) { ui->userInput->setFocus(); return; @@ -229,20 +295,34 @@ void LoginForm::startAuthentication() ui->passwordInput->setFocus(); return; } + startAuthAs(username, ui->passwordInput->text()); + ui->passwordInput->setText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + ui->passwordInput->clear(); +} +void LoginForm::startAuthAs(const QString &user, const QString &pass) +{ + if (Global::testMode()) { + showMessage(QLatin1String("Test mode..."), true); + return; + } + if (Global::greeter()->inAuthentication()) { + Global::greeter()->cancelAuthentication(); + } showMessage(tr("Logging in..."), false); clearMsg = false; - ui->userInput->setEnabled(false); - ui->passwordInput->setEnabled(false); + enableInputs(false); cancelLoginTimer.start(); - Global::greeter()->authenticate(username); + password = pass; + Global::greeter()->authenticate(user); } void LoginForm::onPrompt(QString prompt, QLightDM::Greeter::PromptType /* promptType */) { std::cerr << "Prompt: " << prompt.toStdString() << std::endl; - Global::greeter()->respond(ui->passwordInput->text()); - ui->passwordInput->clear(); + Global::greeter()->respond(password); + password = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + password.clear(); } void LoginForm::leaveDropDownActivated(int index) @@ -298,8 +378,7 @@ void LoginForm::cancelLogin() } cancelLoginTimer.stop(); ui->passwordInput->clear(); - ui->userInput->setEnabled(true); - ui->passwordInput->setEnabled(true); + enableInputs(true); if (!clearMsg) { showMessage(tr("Login failed"), true); } else { @@ -309,6 +388,17 @@ void LoginForm::cancelLogin() clearMsg = true; } +void LoginForm::enableInputs(bool enable) +{ + ui->userInput->setEnabled(enable); + ui->passwordInput->setEnabled(enable); + ui->backButton->setEnabled(enable); + ui->guestButton->setEnabled(enable); + if (browser != nullptr) { + browser->setEnabled(enable); + } +} + void LoginForm::showMessage(QString message, bool error) { hideMessageTimer.stop(); @@ -343,7 +433,7 @@ void LoginForm::keyPressEvent(QKeyEvent *event) return; } if (ui->passwordInput->hasFocus()) { - startAuthentication(); + startFormBasedAuthentication(); return; } } @@ -356,8 +446,9 @@ void LoginForm::keyPressEvent(QKeyEvent *event) void LoginForm::resetForm() { - if (Settings::guestSessionEnabled()) + if (Settings::guestSessionEnabled() || Settings::shibSessionEnabled()) { ui->loginChooser->setCurrentWidget(ui->welcomePage); + } ui->passwordInput->clear(); ui->userInput->clear(); ui->userInput->setFocus(); @@ -370,3 +461,28 @@ bool LoginForm::eventFilter(QObject *object, QEvent *event) } return false; } + +void LoginForm::setBrowserSize() +{ + auto s = ui->shibPage->size(); + auto *f = &shibSize; + if (ui->loginChooser->currentWidget() != ui->shibPage) { + f = &origSize; + s = QSize(50, 50); + } + if (browser != nullptr) { + browser->setFixedSize(s); + browser->setGeometry(QRect(QPoint(0, 0), s)); + } + QTimer::singleShot(10, [=]() { + int pw = 0, ph = 0; + if (this->parentWidget() != nullptr) { + this->parentWidget()->pos(); + pw = (this->parentWidget()->width() - f->width()) / 2; + ph = (this->parentWidget()->height() - f->height()) / 2; + } + this->resize(*f); + this->setFixedSize(*f); + this->setGeometry(pw, ph, f->width(), f->height()); + }); +} diff --git a/src/loginform.h b/src/loginform.h index 79a862e..9f65194 100644 --- a/src/loginform.h +++ b/src/loginform.h @@ -26,6 +26,8 @@ namespace Ui class LoginForm; } +class WebView; + class LoginForm : public QWidget { Q_OBJECT @@ -36,17 +38,25 @@ public: virtual void setFocus(Qt::FocusReason reason); public slots: - void startAuthentication(); + void startFormBasedAuthentication(); void leaveDropDownActivated(int index); void onPrompt(QString prompt, QLightDM::Greeter::PromptType promptType); void onMessage(QString prompt, QLightDM::Greeter::MessageType messageType); void onAuthenticationComplete(); void cancelLogin(); void hideMessage(); + void startAuthAs(const QString &user, const QString &pass); + +private slots: + void setBrowserSize(); + +signals: + void resized(); protected: virtual void keyPressEvent(QKeyEvent *event) override; virtual bool eventFilter(QObject *object, QEvent *event) override; + virtual void resizeEvent(QResizeEvent *) override; private: void initialize(); @@ -56,6 +66,7 @@ private: void setCurrentSession(QString session); void showMessage(QString message, bool error); void resetForm(); + void enableInputs(bool enable); Ui::LoginForm *ui; QMap powerSlots; @@ -63,6 +74,10 @@ private: QTimer cancelLoginTimer; QTimer hideMessageTimer; QTimer resetFormTimer; + QString password; + QSize shibSize, origSize; + + WebView *browser; bool clearMsg; int capsOn; diff --git a/src/loginform.ui b/src/loginform.ui index fcf7f6a..587c0d0 100644 --- a/src/loginform.ui +++ b/src/loginform.ui @@ -7,11 +7,11 @@ 0 0 386 - 296 + 316 - + 0 0 @@ -100,7 +100,13 @@ QComboBox QAbstractItemView::item { 0 - 50 + 45 + + + + + 16777215 + 128 @@ -130,7 +136,7 @@ QComboBox QAbstractItemView::item { 0 - 60 + 30 @@ -173,7 +179,7 @@ QComboBox QAbstractItemView::item { 10 - 50 + 30 @@ -215,7 +221,7 @@ QComboBox QAbstractItemView::item { 0 - + @@ -235,7 +241,7 @@ QComboBox QAbstractItemView::item { - + @@ -255,6 +261,52 @@ QComboBox QAbstractItemView::item { + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + 0 + 50 + + + + + 10 + 75 + true + + + + Shibboleth-Login + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -272,7 +324,7 @@ QComboBox QAbstractItemView::item { 0 - + 350 @@ -293,28 +345,29 @@ QComboBox QAbstractItemView::item { true - - - - QLineEdit::Password - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + QLineEdit::Normal - password - - - 0 - - - 10 + user id - + + + Qt::Vertical + + + + 0 + 0 + + + + + + 350 @@ -335,14 +388,39 @@ QComboBox QAbstractItemView::item { true + + + - QLineEdit::Normal + QLineEdit::Password + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - user id + password + + + 0 + + + 10 + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -360,9 +438,9 @@ QComboBox QAbstractItemView::item { 0 - + - + 0 0 @@ -370,25 +448,30 @@ QComboBox QAbstractItemView::item { 0 - 50 + 45 10 - 75 - true + true - Starten einer Gast-Sitzung + Wenn Sie sich als Gast einloggen, dürfen Sie nur die Webseiten der Universtität Freiburg besuchen. + + + Qt::AlignCenter + + + true - - + + - + 0 0 @@ -396,28 +479,65 @@ QComboBox QAbstractItemView::item { 0 - 45 + 50 10 - true + 75 + true - Wenn Sie sich als Gast einloggen, dürfen Sie nur die Webseiten der Universtität Freiburg besuchen. + Starten einer Gast-Sitzung - - Qt::AlignCenter + + + + + + Qt::Vertical - - true + + + 0 + 0 + - + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2a56fbc..95d74de 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -55,6 +55,7 @@ MainWindow::MainWindow(bool primary, int screen, const QRect &screenRect, QWidge int centerX = screenRect.width()/2 + screenRect.x(); int centerY = screenRect.height()/2 + screenRect.y(); QCursor::setPos(centerX, centerY); + connect(m_LoginForm, &LoginForm::resized, this, &MainWindow::reLayout); } // Banner @@ -80,6 +81,11 @@ void MainWindow::resizeEvent(QResizeEvent *event) QWidget::resizeEvent(event); m_ScreenRect = QRect(this->pos(), event->size()); setBackground(); + reLayout(); +} + +void MainWindow::reLayout() +{ /* * Everything is layed out manually here, since I don't know how to represent the size constraints * and interactions of everything using layout classes. You're welcome to improve this, but I double @@ -177,8 +183,11 @@ void MainWindow::paintEvent(QPaintEvent *event) void MainWindow::mouseDoubleClickEvent(QMouseEvent *) { + static int clicks = 0; if (m_Snake == nullptr) { - m_Snake = new GameCore(this); + if (clicks++ > 0) { + m_Snake = new GameCore(this); + } } else { m_Snake->addSnake(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index e1a8011..3ea0e4a 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -51,6 +51,7 @@ protected: public slots: void updateClock(); + void reLayout(); private: diff --git a/src/settings.h b/src/settings.h index 69554c4..1b3abfd 100644 --- a/src/settings.h +++ b/src/settings.h @@ -35,16 +35,18 @@ public: static QString bannerImageFile() { return s_settings->value("greeter-banner-image").toString(); } static QString bottomLeftLogoFile() { return s_settings->value("greeter-bottom-left-logo-file").toString(); } static QString bottomLeftLogoDir() { return s_settings->value("greeter-bottom-left-logo-path").toString(); } - static QStringList gradientColors() { return s_settings->value("greeter-background-gradient").toString().split(QRegExp("\\s"), QString::SkipEmptyParts); } + static QStringList gradientColors() { return s_settings->value("greeter-background-gradient").toString().split(QRegExp("\\s"), Qt::SkipEmptyParts); } static QString logMessageFile() { return s_settings->value("greeter-message-file").toString(); } static QString newsHtmlFile() { return s_settings->value("news-html-file").toString(); } static QString autoLoginCheckCmd() { return s_settings->value("auto-login-check-cmd").toString(); } static QString clockStyle() { return s_settings->value("clock-text-style").toString(); } static QString clockBackgroundStyle() { return s_settings->value("clock-background-style").toString(); } - static QStringList clockShadow() { return s_settings->value("clock-shadow").toString().split(QRegExp("\\s"), QString::SkipEmptyParts); } + static QStringList clockShadow() { return s_settings->value("clock-shadow").toString().split(QRegExp("\\s"), Qt::SkipEmptyParts); } static QString usernamePlaceholder() { return s_settings->value("username-placeholder").toString(); } static QString passwordPlaceholder() { return s_settings->value("password-placeholder").toString(); } static bool guestSessionEnabled() { return s_settings->value("guest-session-enabled").toBool(); } + static bool shibSessionEnabled() { return s_settings->value("shib-session-enabled").toBool(); } + static QString shibUrl() { return s_settings->value("shib-url").toString(); } static QString userSessionButtonText() { return s_settings->value("user-session-button-text").toString(); } static QString guestSessionButtonText() { return s_settings->value("guest-session-button-text").toString(); } static QString guestSessionStartText() { return s_settings->value("guest-session-start-text").toString(); } diff --git a/src/webview.cpp b/src/webview.cpp new file mode 100644 index 0000000..0ca7328 --- /dev/null +++ b/src/webview.cpp @@ -0,0 +1,109 @@ +#include "webview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +WebView::WebView(QWidget* parent) + : QWebView(parent), + _timerAbortMessage(new QTimer(this)), + _abortedDownload(false), + _timerReset(new QTimer(this)) + { + _timerAbortMessage->setSingleShot(true); + _timerReset->setSingleShot(true); + connect(page(), SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); + page()->setForwardUnsupportedContent(true); + connect(page(), SIGNAL(unsupportedContent(QNetworkReply*)),this,SLOT(unsupportedContent(QNetworkReply*))); + connect(page(), SIGNAL(downloadRequested(QNetworkRequest)),this,SLOT(downloadRequest(QNetworkRequest))); + connect(_timerAbortMessage, &QTimer::timeout, this, &WebView::downloadDeniedMessage); + connect(_timerReset, &QTimer::timeout, this, [this]() { + emit triggerReset(tr("Inactivity Timeout")); + this->stop(); + this->page()->mainFrame()->setContent(""); + }); + connect(this, &QWebView::loadFinished, this, &WebView::onLoadFinished); +} + +void WebView::windowCloseRequested() +{ + // If we have an old URL stored on the stack, navigate back to it, otherwise we return and nothing happens + if (_urls.empty()) + return; + QUrl url = _urls.pop(); + page()->mainFrame()->load(url); +} + +QWebView* WebView::createWindow(QWebPage::WebWindowType) +{ + // Remember current URL, then return the current Web View so no new window opens + _urls.push(this->url()); + return this; +} + +void WebView::unsupportedContent(QNetworkReply* rep) +{ + _abortedDownload = true; + rep->abort(); + rep->deleteLater(); + _timerAbortMessage->start(1); +} + +void WebView::downloadRequest(QNetworkRequest) +{ + _timerAbortMessage->start(1); +} + +void WebView::downloadDeniedMessage() +{ + QMessageBox::warning(this->parentWidget(), QString::fromUtf8("Denied"), + QString::fromUtf8("The requested action triggered a download, which is not allowed.\n\n" + "Diese Aktion löst einen Download aus, was nicht erlaubt ist.")); +} + +void WebView::onLoadFinished(bool ok) +{ + if (_abortedDownload || !ok) { + _abortedDownload = false; + _timerReset->start(10000); + return; + } + auto user = this->page()->mainFrame()->documentElement().findFirst("#bwlp-username"); + auto pass = this->page()->mainFrame()->documentElement().findFirst("#bwlp-password"); + auto err = this->page()->mainFrame()->documentElement().findFirst("#bwlp-error"); + if (!user.isNull() && !pass.isNull()) { + emit startAuthentication(user.toPlainText(), "shib=" + _token + pass.toPlainText()); + } else if (!err.isNull()) { + emit triggerReset(err.toPlainText()); + this->stop(); + this->page()->mainFrame()->setContent(""); + } else { + _timerReset->start(60000); + } +} + +void WebView::reset(const QString baseUrl) +{ + QUrl url(baseUrl); + QUrlQuery q(url.query()); + q.addQueryItem("action", "browser"); + QByteArray input; + input.append((const char*)this, sizeof(*this)); + input.append(QString().sprintf("%d %d", QCursor::pos().x(), QCursor::pos().y())); + input.append(QString::number(QDateTime::currentMSecsSinceEpoch())); + _token = QCryptographicHash::hash(input, QCryptographicHash::Md5).chopped(8).toHex(); + q.addQueryItem("token", _token); + url.setQuery(q); + _urls.clear(); + this->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar); + this->history()->clear(); + this->setUrl(url); + _timerAbortMessage->stop(); + _timerReset->stop(); +} diff --git a/src/webview.h b/src/webview.h new file mode 100644 index 0000000..79b4a64 --- /dev/null +++ b/src/webview.h @@ -0,0 +1,44 @@ +#ifndef WEBVIEW_H_ +#define WEBVIEW_H_ + +#include +#include +#include + +class QNetworkReply; +class QTimer; + +/** + * Make sure pages that want to load in a new tab are actually loaded in the same page, + * and remember the previous URL in case the "new tab" requests to be closed. + */ +class WebView : public QWebView +{ +Q_OBJECT +public: + WebView(QWidget* parent = NULL); + void reset(const QString baseUrl); + +protected: + QWebView *createWindow(QWebPage::WebWindowType); + +signals: + void triggerReset(const QString &message); + void startAuthentication(const QString &user, const QString &pass); + +protected slots: + void windowCloseRequested(); + void unsupportedContent(QNetworkReply*); + void downloadRequest(QNetworkRequest); + void downloadDeniedMessage(); + void onLoadFinished(bool ok); + +private: + QStack _urls; + QTimer *_timerAbortMessage; + bool _abortedDownload; + QString _token; + QTimer *_timerReset; +}; + +#endif diff --git a/src/x11util.h b/src/x11util.h index 6699074..7934d20 100644 --- a/src/x11util.h +++ b/src/x11util.h @@ -4,7 +4,7 @@ #include extern "C" { - #include + typedef struct _XDisplay Display; void AddPixmapToBackground(unsigned const char* imgData, const unsigned int width, const unsigned int height, const unsigned int depth, const int bytesPerLine, const size_t byteCount); unsigned int getKeyMask(Display *dpy); -- cgit v1.2.3-55-g7522