-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, --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

View File

@ -3,9 +3,27 @@
#include <X11/extensions/XTest.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 :)
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);
}

View File

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

View File

@ -8,8 +8,8 @@
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
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);
}
}

View File

@ -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,6 +31,8 @@ LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QSt
}
#endif
//skip GUI initialization in headless mode
if (!headless) {
//prepare the popup first.
fillPopup();
@ -52,6 +54,7 @@ LayoutManager::LayoutManager( bool useTrayIcon, const QString &devdir, const QSt
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);
}
if (!isHeadless) {
fillPopup();
}
if (le) {
le->updateJoypadWidgets();
}
@ -359,7 +364,9 @@ void LayoutManager::saveAs() {
save(file);
//add the new name to our lists
if (!isHeadless) {
fillPopup();
}
if (le) {
le->updateLayoutList();
}
@ -402,7 +409,9 @@ void LayoutManager::importLayout() {
}
QFile::copy(sourceFile, filename);
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);
}
if (!isHeadless) {
fillPopup();
}
if (le) {
le->updateLayoutList();
@ -483,7 +494,9 @@ void LayoutManager::rename() {
return;
}
if (!isHeadless) {
fillPopup();
}
if (le) {
le->updateLayoutList();
}
@ -504,6 +517,8 @@ QStringList LayoutManager::getLayoutNames() const {
}
void LayoutManager::setLayoutName(const QString& name) {
//skip GUI operations in headless mode
if (layoutGroup) {
QList<QAction*> actions = layoutGroup->actions();
for (int i = 0; i < actions.size(); ++ i) {
QAction* action = actions[i];
@ -512,6 +527,7 @@ void LayoutManager::setLayoutName(const QString& name) {
break;
}
}
}
currentLayout = name;
if (le) {
@ -672,7 +688,9 @@ void LayoutManager::updateJoyDevs() {
#endif
//when it's all done, rebuild the popup menu so it displays the correct
//information.
if (!isHeadless) {
fillPopup();
}
if (le) {
le->updateJoypadWidgets();
}

View File

@ -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<int, JoyPad*> available;
QHash<int, JoyPad*> joypads;
bool isNotrayMode;
bool isHeadless;
#ifdef WITH_LIBUDEV
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.
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;
}