diff --git a/README.md b/README.md index b73db63..e12d4e4 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ Layout files use standard X11 keycodes and are human-readable. You can edit them - `-t, --tray`: Use system tray icon (instead of default window mode) - `-T, --notray`: Use window mode (default behavior) - `-u, --update`: Update device list in running instance +- `-H, --headless=LAYOUT`: Run in headless mode with no GUI, loading the specified layout **Layout Name:** Load the specified layout on startup diff --git a/src/event.cpp b/src/event.cpp index 115acff..17ca9d7 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -3,9 +3,27 @@ #include #include "event.h" +//persistent X11 display connection to avoid opening/closing for each event +static Display* g_display = nullptr; + +static Display* getDisplay() { + if (!g_display) { + g_display = XOpenDisplay(nullptr); + } + return g_display; +} + +//cleanup function called at application exit +void cleanupDisplay() { + if (g_display) { + XCloseDisplay(g_display); + g_display = nullptr; + } +} + //actually creates an XWindows event :) void sendevent(const FakeEvent &e) { - Display* display = XOpenDisplay(nullptr); + Display* display = getDisplay(); if (!display) return; switch (e.type) { @@ -48,5 +66,4 @@ void sendevent(const FakeEvent &e) { break; } XFlush(display); - XCloseDisplay(display); } diff --git a/src/event.h b/src/event.h index 5637a56..0228519 100644 --- a/src/event.h +++ b/src/event.h @@ -22,5 +22,6 @@ struct FakeEvent { }; void sendevent(const FakeEvent& e); +void cleanupDisplay(); #endif diff --git a/src/joypad.cpp b/src/joypad.cpp index 05f1f96..6cfc6d1 100644 --- a/src/joypad.cpp +++ b/src/joypad.cpp @@ -8,8 +8,8 @@ #include #include #include -#include #include +#include #include JoyPad::JoyPad( int i, int dev, QObject *parent ) @@ -96,7 +96,7 @@ void JoyPad::open(int dev) { readNotifier = new QSocketNotifier(joydev, QSocketNotifier::Read, this); connect(readNotifier, SIGNAL(activated(int)), this, SLOT(handleJoyEvents())); errorNotifier = new QSocketNotifier(joydev, QSocketNotifier::Exception, this); - connect(errorNotifier, SIGNAL(activated(int)), this, SLOT(handleJoyEvents())); + connect(errorNotifier, SIGNAL(activated(int)), this, SLOT(errorRead())); debug_mesg("Done setting up joyDeviceListeners\n"); debug_mesg("done resetting to dev\n"); } @@ -259,6 +259,18 @@ void JoyPad::handleJoyEvents() { if (len == sizeof(js_event)) { //pass that event on to the joypad! jsevent(msg); + } else if (len < 0) { + //handle read errors + if (errno == EAGAIN || errno == EWOULDBLOCK) { + //normal for non-blocking read when no data available + return; + } + //device error - trigger error handling + debug_mesg("Read error on joystick device fd %d: %s\n", joydev, strerror(errno)); + errorRead(); + } else if (len > 0) { + //partial read - should not happen with joystick events + debug_mesg("Warning: partial read (%zd bytes) from joystick device fd %d\n", len, joydev); } } diff --git a/src/layout.cpp b/src/layout.cpp index 1b90549..5b327ba 100644 --- a/src/layout.cpp +++ b/src/layout.cpp @@ -11,14 +11,14 @@ //initialize things and set up an icon :) -LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QString &settingsDir ) +LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QString &settingsDir, bool headless ) : devdir(devdir), settingsDir(settingsDir), - layoutGroup(new QActionGroup(this)), - updateDevicesAction(new QAction(QIcon::fromTheme("view-refresh"),"Update &Joystick Devices",this)), - updateLayoutsAction(new QAction(QIcon::fromTheme("view-refresh"),"Update &Layout List",this)), - addNewConfiguration(new QAction(QIcon::fromTheme("list-add"),"Add new configuration",this)), - quitAction(new QAction(QIcon::fromTheme("application-exit"),"&Quit",this)), - le(0), isNotrayMode(!useTrayIcon) { + layoutGroup(headless ? 0 : new QActionGroup(this)), + updateDevicesAction(headless ? 0 : new QAction(QIcon::fromTheme("view-refresh"),"Update &Joystick Devices",this)), + updateLayoutsAction(headless ? 0 : new QAction(QIcon::fromTheme("view-refresh"),"Update &Layout List",this)), + addNewConfiguration(headless ? 0 : new QAction(QIcon::fromTheme("list-add"),"Add new configuration",this)), + quitAction(headless ? 0 : new QAction(QIcon::fromTheme("application-exit"),"&Quit",this)), + le(0), isNotrayMode(!useTrayIcon), isHeadless(headless) { #ifdef WITH_LIBUDEV udevNotifier = 0; @@ -31,27 +31,30 @@ LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QSt } #endif - //prepare the popup first. - fillPopup(); + //skip GUI initialization in headless mode + if (!headless) { + //prepare the popup first. + fillPopup(); - //make a tray icon - if (useTrayIcon) { - QSystemTrayIcon *tray = new QSystemTrayIcon(this); - tray->setContextMenu(&trayMenu); - tray->setIcon(QIcon(THUNDERPAD_ICON24)); - tray->show(); - connect(tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayClick(QSystemTrayIcon::ActivationReason))); - } - //in notray mode, just show the configuration window directly - else { - // Automatically show the configuration window for accessibility - addNewConfig(); - } + //make a tray icon + if (useTrayIcon) { + QSystemTrayIcon *tray = new QSystemTrayIcon(this); + tray->setContextMenu(&trayMenu); + tray->setIcon(QIcon(THUNDERPAD_ICON24)); + tray->show(); + connect(tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayClick(QSystemTrayIcon::ActivationReason))); + } + //in notray mode, just show the configuration window directly + else { + // Automatically show the configuration window for accessibility + addNewConfig(); + } - connect(updateLayoutsAction, SIGNAL(triggered()), this, SLOT(fillPopup())); - connect(updateDevicesAction, SIGNAL(triggered()), this, SLOT(updateJoyDevs())); - connect(addNewConfiguration, SIGNAL(triggered()), this, SLOT(addNewConfig())); - connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); + connect(updateLayoutsAction, SIGNAL(triggered()), this, SLOT(fillPopup())); + connect(updateDevicesAction, SIGNAL(triggered()), this, SLOT(updateJoyDevs())); + connect(addNewConfiguration, SIGNAL(triggered()), this, SLOT(addNewConfig())); + connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); + } //no layout loaded at start. setLayoutName(QString()); @@ -146,7 +149,9 @@ void LayoutManager::udevUpdate() { addJoyPad(index, path); } - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateJoypadWidgets(); } @@ -359,7 +364,9 @@ void LayoutManager::saveAs() { save(file); //add the new name to our lists - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateLayoutList(); } @@ -402,7 +409,9 @@ void LayoutManager::importLayout() { } QFile::copy(sourceFile, filename); - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateLayoutList(); } @@ -444,7 +453,9 @@ void LayoutManager::remove() { if (!QFile(filename).remove()) { errorBox(tr("Remove error"), tr("Could not remove file %1").arg(filename), le); } - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateLayoutList(); @@ -483,7 +494,9 @@ void LayoutManager::rename() { return; } - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateLayoutList(); } @@ -504,12 +517,15 @@ QStringList LayoutManager::getLayoutNames() const { } void LayoutManager::setLayoutName(const QString& name) { - QList actions = layoutGroup->actions(); - for (int i = 0; i < actions.size(); ++ i) { - QAction* action = actions[i]; - if (action->data().toString() == name) { - action->setChecked(true); - break; + //skip GUI operations in headless mode + if (layoutGroup) { + QList actions = layoutGroup->actions(); + for (int i = 0; i < actions.size(); ++ i) { + QAction* action = actions[i]; + if (action->data().toString() == name) { + action->setChecked(true); + break; + } } } currentLayout = name; @@ -672,7 +688,9 @@ void LayoutManager::updateJoyDevs() { #endif //when it's all done, rebuild the popup menu so it displays the correct //information. - fillPopup(); + if (!isHeadless) { + fillPopup(); + } if (le) { le->updateJoypadWidgets(); } diff --git a/src/layout.h b/src/layout.h index fe3956d..942f281 100644 --- a/src/layout.h +++ b/src/layout.h @@ -38,7 +38,7 @@ class LayoutManager : public QObject { friend class LayoutEdit; Q_OBJECT public: - LayoutManager(bool useTrayIcon, const QString &devdir, const QString &settingsDir); + LayoutManager(bool useTrayIcon, const QString &devdir, const QString &settingsDir, bool headless = false); ~LayoutManager(); //produces a list of the names of all the available layout. @@ -111,6 +111,7 @@ class LayoutManager : public QObject { QHash available; QHash joypads; bool isNotrayMode; + bool isHeadless; #ifdef WITH_LIBUDEV bool initUDev(); diff --git a/src/main.cpp b/src/main.cpp index 11a589f..cac2ade 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -73,6 +73,8 @@ int main( int argc, char **argv ) //this execution wasn't made to update the joystick device list. bool update = false; bool forceTrayIcon = false; + //headless mode - no GUI at all + bool headless = false; //parse command-line options struct option long_options[] = { @@ -81,11 +83,12 @@ int main( int argc, char **argv ) {"tray", no_argument, 0, 't'}, {"notray", no_argument, 0, 'T'}, {"update", no_argument, 0, 'u'}, + {"headless", required_argument, 0, 'H'}, {0, 0, 0, 0 } }; for (;;) { - int c = getopt_long(argc, argv, "hd:tTu", long_options, NULL); + int c = getopt_long(argc, argv, "hd:tTuH:", long_options, NULL); if (c == -1) break; @@ -93,7 +96,7 @@ int main( int argc, char **argv ) switch (c) { case 'h': printf("%s\n" - "Usage: %s [--device=\"/device/path\"] [--tray|--notray] [\"layout name\"]\n" + "Usage: %s [--device=\"/device/path\"] [--tray|--notray] [--headless=LAYOUT] [\"layout name\"]\n" "\n" "Options:\n" " -h, --help Print this help message.\n" @@ -104,6 +107,8 @@ int main( int argc, char **argv ) " -T, --notray Do not use a system tray icon (default).\n" " -u, --update Force a running instance of ThunderPad to update its\n" " list of devices and layouts.\n" + " -H, --headless=LAYOUT Run in headless mode with no GUI, loading the\n" + " specified layout for joystick-to-keyboard translation.\n" " \"layout name\" Load the given layout in an already running\n" " instance of ThunderPad, or start ThunderPad using the\n" " given layout.\n", @@ -134,6 +139,11 @@ int main( int argc, char **argv ) update = true; break; + case 'H': + headless = true; + layout = optarg; + break; + case '?': fprintf(stderr, "Illegal argument.\n" "See `%s --help` for more information\n", argc > 0 ? argv[0] : "thunderpad"); @@ -151,6 +161,14 @@ int main( int argc, char **argv ) } } + //validate headless mode requirements + if (headless && layout.isEmpty()) { + fprintf(stderr, "Headless mode requires a layout to be specified.\n" + "Use -H LAYOUT or --headless=LAYOUT\n" + "See `%s --help` for more information\n", argc > 0 ? argv[0] : "thunderpad"); + return 1; + } + //if the user specified a layout to use, if (!layout.isEmpty()) { @@ -169,7 +187,7 @@ int main( int argc, char **argv ) //create a pid lock file. - QFile pidFile( "/tmp/thunderpad.pid" ); + QFile pidFile( headless ? "/tmp/thunderpad-headless.pid" : "/tmp/thunderpad.pid" ); //if that file already exists, then thunderpad is already running! if (pidFile.exists()) { @@ -185,10 +203,11 @@ int main( int argc, char **argv ) //then prevent two instances from running at once. //however, if we are setting the layout or updating the device //list, this is not an error and we shouldn't make one! - if (layout.isEmpty() && !update) + //headless mode also allows multiple instances + if (layout.isEmpty() && !update && !headless) errorBox("Instance Error", "There is already a running instance of ThunderPad; please close\nthe old instance before starting a new one."); - else { + else if (!headless) { //if one of these is the case, send the appropriate signal! if (update) { kill(pid,SIGUSR1); @@ -197,8 +216,8 @@ int main( int argc, char **argv ) kill(pid,SIGUSR2); } } - //and quit. We don't need two instances. - return 0; + //and quit. We don't need two instances (unless headless). + if (!headless) return 0; } } } @@ -223,7 +242,7 @@ int main( int argc, char **argv ) } //create a new LayoutManager with a tray icon / floating icon, depending //on the user's request - LayoutManager layoutManager(useTrayIcon,devdir,settingsDir); + LayoutManager layoutManager(useTrayIcon,devdir,settingsDir,headless); layoutManagerPtr = &layoutManager; //build the joystick device list for the first time, @@ -246,6 +265,9 @@ int main( int argc, char **argv ) //remove the lock file... pidFile.remove(); + //cleanup X11 display connection + cleanupDisplay(); + //and terminate! return result; }