-H flag added for headless mode. Requires profile name without the .lyt extension.
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
@ -22,5 +22,6 @@ struct FakeEvent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void sendevent(const FakeEvent& e);
|
void sendevent(const FakeEvent& e);
|
||||||
|
void cleanupDisplay();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
38
src/main.cpp
38
src/main.cpp
@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user