-H flag added for headless mode. Requires profile name without the .lyt extension.

This commit is contained in:
Storm Dragon
2025-06-30 21:16:57 -04:00
parent 811bae82d7
commit a0f6f34701
7 changed files with 123 additions and 51 deletions

View File

@ -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, --tray`: Use system tray icon (instead of default window mode)
- `-T, --notray`: Use window mode (default behavior) - `-T, --notray`: Use window mode (default behavior)
- `-u, --update`: Update device list in running instance - `-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 **Layout Name:** Load the specified layout on startup

View File

@ -3,9 +3,27 @@
#include <X11/extensions/XTest.h> #include <X11/extensions/XTest.h>
#include "event.h" #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 :) //actually creates an XWindows event :)
void sendevent(const FakeEvent &e) { void sendevent(const FakeEvent &e) {
Display* display = XOpenDisplay(nullptr); Display* display = getDisplay();
if (!display) return; if (!display) return;
switch (e.type) { switch (e.type) {
@ -48,5 +66,4 @@ void sendevent(const FakeEvent &e) {
break; break;
} }
XFlush(display); XFlush(display);
XCloseDisplay(display);
} }

View File

@ -22,5 +22,6 @@ struct FakeEvent {
}; };
void sendevent(const FakeEvent& e); void sendevent(const FakeEvent& e);
void cleanupDisplay();
#endif #endif

View File

@ -8,8 +8,8 @@
#include <fcntl.h> #include <fcntl.h>
#include <errno.h> #include <errno.h>
#include <unistd.h> #include <unistd.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include <stdio.h>
#include <stdint.h> #include <stdint.h>
JoyPad::JoyPad( int i, int dev, QObject *parent ) JoyPad::JoyPad( int i, int dev, QObject *parent )
@ -96,7 +96,7 @@ void JoyPad::open(int dev) {
readNotifier = new QSocketNotifier(joydev, QSocketNotifier::Read, this); readNotifier = new QSocketNotifier(joydev, QSocketNotifier::Read, this);
connect(readNotifier, SIGNAL(activated(int)), this, SLOT(handleJoyEvents())); connect(readNotifier, SIGNAL(activated(int)), this, SLOT(handleJoyEvents()));
errorNotifier = new QSocketNotifier(joydev, QSocketNotifier::Exception, this); 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 setting up joyDeviceListeners\n");
debug_mesg("done resetting to dev\n"); debug_mesg("done resetting to dev\n");
} }
@ -259,6 +259,18 @@ void JoyPad::handleJoyEvents() {
if (len == sizeof(js_event)) { if (len == sizeof(js_event)) {
//pass that event on to the joypad! //pass that event on to the joypad!
jsevent(msg); 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);
} }
} }

View File

@ -11,14 +11,14 @@
//initialize things and set up an icon :) //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), : devdir(devdir), settingsDir(settingsDir),
layoutGroup(new QActionGroup(this)), layoutGroup(headless ? 0 : new QActionGroup(this)),
updateDevicesAction(new QAction(QIcon::fromTheme("view-refresh"),"Update &Joystick Devices",this)), updateDevicesAction(headless ? 0 : new QAction(QIcon::fromTheme("view-refresh"),"Update &Joystick Devices",this)),
updateLayoutsAction(new QAction(QIcon::fromTheme("view-refresh"),"Update &Layout List",this)), updateLayoutsAction(headless ? 0 : new QAction(QIcon::fromTheme("view-refresh"),"Update &Layout List",this)),
addNewConfiguration(new QAction(QIcon::fromTheme("list-add"),"Add new configuration",this)), addNewConfiguration(headless ? 0 : new QAction(QIcon::fromTheme("list-add"),"Add new configuration",this)),
quitAction(new QAction(QIcon::fromTheme("application-exit"),"&Quit",this)), quitAction(headless ? 0 : new QAction(QIcon::fromTheme("application-exit"),"&Quit",this)),
le(0), isNotrayMode(!useTrayIcon) { le(0), isNotrayMode(!useTrayIcon), isHeadless(headless) {
#ifdef WITH_LIBUDEV #ifdef WITH_LIBUDEV
udevNotifier = 0; udevNotifier = 0;
@ -31,27 +31,30 @@ LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QSt
} }
#endif #endif
//prepare the popup first. //skip GUI initialization in headless mode
fillPopup(); if (!headless) {
//prepare the popup first.
fillPopup();
//make a tray icon //make a tray icon
if (useTrayIcon) { if (useTrayIcon) {
QSystemTrayIcon *tray = new QSystemTrayIcon(this); QSystemTrayIcon *tray = new QSystemTrayIcon(this);
tray->setContextMenu(&trayMenu); tray->setContextMenu(&trayMenu);
tray->setIcon(QIcon(THUNDERPAD_ICON24)); tray->setIcon(QIcon(THUNDERPAD_ICON24));
tray->show(); tray->show();
connect(tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayClick(QSystemTrayIcon::ActivationReason))); connect(tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayClick(QSystemTrayIcon::ActivationReason)));
} }
//in notray mode, just show the configuration window directly //in notray mode, just show the configuration window directly
else { else {
// Automatically show the configuration window for accessibility // Automatically show the configuration window for accessibility
addNewConfig(); addNewConfig();
} }
connect(updateLayoutsAction, SIGNAL(triggered()), this, SLOT(fillPopup())); connect(updateLayoutsAction, SIGNAL(triggered()), this, SLOT(fillPopup()));
connect(updateDevicesAction, SIGNAL(triggered()), this, SLOT(updateJoyDevs())); connect(updateDevicesAction, SIGNAL(triggered()), this, SLOT(updateJoyDevs()));
connect(addNewConfiguration, SIGNAL(triggered()), this, SLOT(addNewConfig())); connect(addNewConfiguration, SIGNAL(triggered()), this, SLOT(addNewConfig()));
connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
}
//no layout loaded at start. //no layout loaded at start.
setLayoutName(QString()); setLayoutName(QString());
@ -146,7 +149,9 @@ void LayoutManager::udevUpdate() {
addJoyPad(index, path); addJoyPad(index, path);
} }
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateJoypadWidgets(); le->updateJoypadWidgets();
} }
@ -359,7 +364,9 @@ void LayoutManager::saveAs() {
save(file); save(file);
//add the new name to our lists //add the new name to our lists
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateLayoutList(); le->updateLayoutList();
} }
@ -402,7 +409,9 @@ void LayoutManager::importLayout() {
} }
QFile::copy(sourceFile, filename); QFile::copy(sourceFile, filename);
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateLayoutList(); le->updateLayoutList();
} }
@ -444,7 +453,9 @@ void LayoutManager::remove() {
if (!QFile(filename).remove()) { if (!QFile(filename).remove()) {
errorBox(tr("Remove error"), tr("Could not remove file %1").arg(filename), le); errorBox(tr("Remove error"), tr("Could not remove file %1").arg(filename), le);
} }
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateLayoutList(); le->updateLayoutList();
@ -483,7 +494,9 @@ void LayoutManager::rename() {
return; return;
} }
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateLayoutList(); le->updateLayoutList();
} }
@ -504,12 +517,15 @@ QStringList LayoutManager::getLayoutNames() const {
} }
void LayoutManager::setLayoutName(const QString& name) { void LayoutManager::setLayoutName(const QString& name) {
QList<QAction*> actions = layoutGroup->actions(); //skip GUI operations in headless mode
for (int i = 0; i < actions.size(); ++ i) { if (layoutGroup) {
QAction* action = actions[i]; QList<QAction*> actions = layoutGroup->actions();
if (action->data().toString() == name) { for (int i = 0; i < actions.size(); ++ i) {
action->setChecked(true); QAction* action = actions[i];
break; if (action->data().toString() == name) {
action->setChecked(true);
break;
}
} }
} }
currentLayout = name; currentLayout = name;
@ -672,7 +688,9 @@ void LayoutManager::updateJoyDevs() {
#endif #endif
//when it's all done, rebuild the popup menu so it displays the correct //when it's all done, rebuild the popup menu so it displays the correct
//information. //information.
fillPopup(); if (!isHeadless) {
fillPopup();
}
if (le) { if (le) {
le->updateJoypadWidgets(); le->updateJoypadWidgets();
} }

View File

@ -38,7 +38,7 @@ class LayoutManager : public QObject {
friend class LayoutEdit; friend class LayoutEdit;
Q_OBJECT Q_OBJECT
public: public:
LayoutManager(bool useTrayIcon, const QString &devdir, const QString &settingsDir); LayoutManager(bool useTrayIcon, const QString &devdir, const QString &settingsDir, bool headless = false);
~LayoutManager(); ~LayoutManager();
//produces a list of the names of all the available layout. //produces a list of the names of all the available layout.
@ -111,6 +111,7 @@ class LayoutManager : public QObject {
QHash<int, JoyPad*> available; QHash<int, JoyPad*> available;
QHash<int, JoyPad*> joypads; QHash<int, JoyPad*> joypads;
bool isNotrayMode; bool isNotrayMode;
bool isHeadless;
#ifdef WITH_LIBUDEV #ifdef WITH_LIBUDEV
bool initUDev(); bool initUDev();

View File

@ -73,6 +73,8 @@ int main( int argc, char **argv )
//this execution wasn't made to update the joystick device list. //this execution wasn't made to update the joystick device list.
bool update = false; bool update = false;
bool forceTrayIcon = false; bool forceTrayIcon = false;
//headless mode - no GUI at all
bool headless = false;
//parse command-line options //parse command-line options
struct option long_options[] = { struct option long_options[] = {
@ -81,11 +83,12 @@ int main( int argc, char **argv )
{"tray", no_argument, 0, 't'}, {"tray", no_argument, 0, 't'},
{"notray", no_argument, 0, 'T'}, {"notray", no_argument, 0, 'T'},
{"update", no_argument, 0, 'u'}, {"update", no_argument, 0, 'u'},
{"headless", required_argument, 0, 'H'},
{0, 0, 0, 0 } {0, 0, 0, 0 }
}; };
for (;;) { 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) if (c == -1)
break; break;
@ -93,7 +96,7 @@ int main( int argc, char **argv )
switch (c) { switch (c) {
case 'h': case 'h':
printf("%s\n" 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" "\n"
"Options:\n" "Options:\n"
" -h, --help Print this help message.\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" " -T, --notray Do not use a system tray icon (default).\n"
" -u, --update Force a running instance of ThunderPad to update its\n" " -u, --update Force a running instance of ThunderPad to update its\n"
" list of devices and layouts.\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" " \"layout name\" Load the given layout in an already running\n"
" instance of ThunderPad, or start ThunderPad using the\n" " instance of ThunderPad, or start ThunderPad using the\n"
" given layout.\n", " given layout.\n",
@ -134,6 +139,11 @@ int main( int argc, char **argv )
update = true; update = true;
break; break;
case 'H':
headless = true;
layout = optarg;
break;
case '?': case '?':
fprintf(stderr, "Illegal argument.\n" fprintf(stderr, "Illegal argument.\n"
"See `%s --help` for more information\n", argc > 0 ? argv[0] : "thunderpad"); "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 the user specified a layout to use,
if (!layout.isEmpty()) if (!layout.isEmpty())
{ {
@ -169,7 +187,7 @@ int main( int argc, char **argv )
//create a pid lock file. //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 that file already exists, then thunderpad is already running!
if (pidFile.exists()) if (pidFile.exists())
{ {
@ -185,10 +203,11 @@ int main( int argc, char **argv )
//then prevent two instances from running at once. //then prevent two instances from running at once.
//however, if we are setting the layout or updating the device //however, if we are setting the layout or updating the device
//list, this is not an error and we shouldn't make one! //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", errorBox("Instance Error",
"There is already a running instance of ThunderPad; please close\nthe old instance before starting a new one."); "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 one of these is the case, send the appropriate signal!
if (update) { if (update) {
kill(pid,SIGUSR1); kill(pid,SIGUSR1);
@ -197,8 +216,8 @@ int main( int argc, char **argv )
kill(pid,SIGUSR2); kill(pid,SIGUSR2);
} }
} }
//and quit. We don't need two instances. //and quit. We don't need two instances (unless headless).
return 0; 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 //create a new LayoutManager with a tray icon / floating icon, depending
//on the user's request //on the user's request
LayoutManager layoutManager(useTrayIcon,devdir,settingsDir); LayoutManager layoutManager(useTrayIcon,devdir,settingsDir,headless);
layoutManagerPtr = &layoutManager; layoutManagerPtr = &layoutManager;
//build the joystick device list for the first time, //build the joystick device list for the first time,
@ -246,6 +265,9 @@ int main( int argc, char **argv )
//remove the lock file... //remove the lock file...
pidFile.remove(); pidFile.remove();
//cleanup X11 display connection
cleanupDisplay();
//and terminate! //and terminate!
return result; return result;
} }