diff --git a/cli-specs.md b/cli-specs.md new file mode 100644 index 0000000..709dcb4 --- /dev/null +++ b/cli-specs.md @@ -0,0 +1,279 @@ +# CLI + +The command line interface is a fully scriptable interface to the user. + +## Idea + +Pass in a specific switch/flag - for example `--cmd` - to enter command mode and interpret subsequent strings as the command to execute. + +## Available operations + +Gather all available operations/commands here to have an overview and a guideline for parsing the given operation strings. + +Additionally there should be a help page to display the available operations and the commands for them. The tooltip text can be used for this endeavour. + +### Settings + +* list all available keyboard layouts +* set keyboard layout +* list available modifier keys (notify of keyboard profile/binding) + * OSX has different source keys! (i.e. Option, Command instead of Alt, Super) +* set modifier key to different key +* reset modifier keys +* display and set _frame rate_ +* display and set _directory of animation scripts_ +* display and set _brightness per-mode_ +* display and set _spatial dithering to simulate extra color resolution_ +* display and set _disable mouse acceleration_ (only on OSX) +* display and set _disable scroll acceleration_ (only on OSX) +* display and set _scroll acceleration speed_ (only on OSX) +* display and set _check for new firmware automatically_ +* display and set _show tray icon_ +* display and set _macro delay_ + +### Keyboard + +* List Profiles +* Create/Rename/Duplicate/Move Profile +* Save Profile to Hardware +* List Modes +* Create/Rename/Duplicate/Move Modes + +#### Lighting + +* display and set _key color_ +* display and set _key brightness_ +* display and set _key animation_ + +#### Binding + +* display, set and/or add _key bind_ + * bind to keyboard input (char, function, media, modifier, etc.) + * bind to mouse input (button, wheel, special button, change DPI, etc.) + * bind to animation (currently assigned animations) + * bind to special (switch mode, brightness, windows lock) + * bind to program (run script/program on press/release) +* copy bindings to mode +* bind macro action, text and comment to key + +#### Performance + +* display and set _indicator intensity_ +* display and set value of _indicator intensity_ +* display and set _indicate brightness_ +* display and set value of _indicate brightness_ (33%, 67%, 100%) +* display and set _indicate windows lock_ +* display and set value of _indicate windows lock_ (on, off) +* display and set _indicate mute_ +* display and set value of _indicate mute_ (on, off, unknown) +* list available modes for lock keys +* display and set _num lock_ +* display and set value of _num lock_ (on, off) +* display and set _caps lock_ +* display and set value of _caps lock_ (on, off) +* display and set _scroll lock_ (display warning because of scroll lock) +* display and set value of _scroll lock_ (on, off) + +#### Device + +* display information about device + * _USB device ID_ + * _Poll Rate_ + * _Firmware Version_ + * _Status_ +* update firmware + * automatically (via internet) + * manually (via file) + +### Mouse + +* List Profiles +* Create/Rename/Duplicate/Move Profile +* Save Profile to Hardware +* List Modes +* Create/Rename/Duplicate/Move Modes + +#### Lighting + +* display and set _key color_ +* display and set _key brightness_ +* display and add _key animation_ + +#### Binding + +* display, set and/or add _key bind_ + * bind to keyboard input (char, function, media, modifier, etc.) + * bind to mouse input (button, wheel, special button, change DPI, etc.) + * bind to animation (currently assigned animations) + * bind to special (switch mode, brightness, windows lock) + * bind to programm (run script/program on press/release) +* copy bindings to mode + +#### Performance + +* display and set _use DPI indicator_ +* display and set _DPI Indicator percentage_ +* display and set _independent X/Y states_ +* list DPI Stages with X/Y values +* display and set stages 1-5 + sniper & other +* display and set _angle snap_ +* list _lift height_ values +* display and set _lift height_ +* copy performance to mode + +#### Device + +* display information about device + * _USB device ID_ + * _Poll Rate_ + * _Firmware Version_ + * _Status_ +* update firmware + * automatically (via internet) + * manually (via file) + +## Grammar + +Generally the command line interface needs a different approach, than the GUI. For example does the GUI display all information concisely, while the _cli_ can only display a set of information at a time triggered by a command. Thus the grouping has to be different than the GUI does it. + +``` + ::= "global" + | "device" [] + | "profile" [] + ::= + ::= + + ::= "up" | "down" + ::= "#" + | "(" "," "," ")" +``` + +### Global Commands + +``` + ::= "info" + | "layout" + | "modifier" + | "framerate" + | "animation-dir" + | "brightness-per-mode" + | "spatial-dithering" + | "firmware-autocheck" + | "tray-icon" + | "mouse-acceleration" + | "scroll-acceleration" + | "scroll-acceleration-speed" + | "long-macro-delay" + + ::= "show" | "set" + ::= "show" | "set" + ::= "show" | "set" + ::= "show" | "enable" | "disable" + + ::= "list" | "set" + ::= "EU" | "EU_DVORAK" + | "GB" | "GB_DVORAK" + | "US" | "US_DVORAK" + | "FR" | "DE" | "IT" | "PL" | "MX" | "ES" | "SE" + + ::= "list" | "set " | "reset" + ::= "lshift" | "lctrl" | "lwin" | "lalt" | "caps" | "menu" + | "rshift" | "rctrl" | "rwin" | "ralt" | "fn" | "altgr" + ::= "caps" | "shift" | "ctrl" | "option" | "cmd" | "alt" | "super" + + ::= | "scan" +``` + +### Device Commands + +``` + ::= "info" + | "show profile" + | "set profile" + | "firmware" + + ::= "show" + | "update" [] +``` + +### Profile Commands + +``` + ::= "list" + | "create" + | "duplicate" + | "rename" + | "move" + | "mode" + ::= + + ::= "list" + | "create" + | "duplicate" + | "rename" + | "move" + | "animation" + | "lighting" + | "bind" + | "performance" + + ::= "list" + | "show" + | "delete" + | "set" + + ::= "brightness" + | "color" + | "animation" + ::= "show" | "set" + ::= "show" | "set" + ::= "show" | "set" + + ::= "list" + | "show" + | "set" + | "copy" + | "reset" + | "set-macro" + | "list-macros" + + ::= + ::= "" + | "+" + | + ::= | "+" + ::= + | + ::= + + ::= + ::= "" | + ::= "" | + ::= "" | + + ::= "copy" + | "intensity" + | "indicator" + | "lock" + | "dpi" + + ::= + ::= "brightness" | "windows-lock" | "mute" + ::= "show" | "enable" | "disable" | set + ::= "0" | "1" | "2" + + ::= "list" + | "indicator" + | "independent-axes" + | "stage" "set" "x" ["y" ] + | "angle-snap" + | "lift-height" + ::= | "set" + ::= "0" | "1" | "2" | "3" | "4" | "5" | "other" + + ::= "show" | + ::= "num" | "caps" | "scroll" + ::= "set" | "set" + ::= "normal" | "always-on" | "always-off" | "RGB" | "normal-rgb" + ::= "0" | "1" +``` diff --git a/src/ckb/ckb.pro b/src/ckb/ckb.pro index 1a6ea11..4a9e783 100644 --- a/src/ckb/ckb.pro +++ b/src/ckb/ckb.pro @@ -101,7 +101,8 @@ SOURCES += main.cpp\ extrasettingswidget.cpp \ kbmanager.cpp \ colormap.cpp \ - macroreader.cpp + macroreader.cpp \ + cli.cpp HEADERS += mainwindow.h \ kbwidget.h \ @@ -159,7 +160,8 @@ HEADERS += mainwindow.h \ extrasettingswidget.h \ kbmanager.h \ colormap.h \ - macroreader.h + macroreader.h \ + cli.h FORMS += mainwindow.ui \ kbwidget.ui \ diff --git a/src/ckb/cli.cpp b/src/ckb/cli.cpp new file mode 100644 index 0000000..1d9629f --- /dev/null +++ b/src/ckb/cli.cpp @@ -0,0 +1,799 @@ +#include "cli.h" +#include "animscript.h" +#include "ckbsettings.h" +#include "keymap.h" +#include "kbmanager.h" +#include "kbbind.h" +#include "kblight.h" +#include +#include + +/** + * resolveCommand - Resolve the given String to a Command identifier. + */ +int Command::resolveCommand(QString cmd) { + QString lowerCmd = cmd.toLower(); + if (lowerCmd.compare("global") == 0) return Command::CommandGlobal; + else if (lowerCmd.compare("device") == 0) return Command::CommandDevice; + else if (lowerCmd.compare("profile") == 0) return Command::CommandProfile; + else if (lowerCmd.compare("help") == 0) return Command::CommandHelp; + else if (lowerCmd.compare("info") == 0) return Command::CommandInfo; + else if (lowerCmd.compare("layout") == 0) return Command::CommandLayout; + else if (lowerCmd.compare("modifier") == 0) return Command::CommandModifier; + else if (lowerCmd.compare("framerate") == 0) return Command::CommandFramerate; + else if (lowerCmd.compare("animation-dir") == 0) return Command::CommandAnimationDir; + else if (lowerCmd.compare("brightness-per-mode") == 0) return Command::CommandBrightnessPerMode; + else if (lowerCmd.compare("spatial-dithering") == 0) return Command::CommandSpatialDithering; + else if (lowerCmd.compare("firmware-autocheck") == 0) return Command::CommandFirmwareAutocheck; + else if (lowerCmd.compare("tray-icon") == 0) return Command::CommandTrayIcon; + else if (lowerCmd.compare("mouse-acceleration") == 0) return Command::CommandMouseAcceleration; + else if (lowerCmd.compare("scroll-acceleration") == 0) return Command::CommandScrollAcceleration; + else if (lowerCmd.compare("scroll-acceleration-speed") == 0) return Command::CommandScrollAccelerationSpeed; + else if (lowerCmd.compare("long-macro-delay") == 0) return Command::CommandLongMacroDelay; + + return Command::CommandUnknown; +} + +int CommandLine::runGlobal() { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + switch (Command::resolveCommand(commands[cmdOffset++])) { + case Command::CommandInfo: + { + // TODO: the next section is almost verbosely taken from `MainWindow::updateVersion()` + // Outsource one of these routines and use it for both frontends. + QString daemonVersion = KbManager::ckbDaemonVersion(); + QString deviceLabel; + if(daemonVersion == DAEMON_UNAVAILABLE_STR){ + deviceLabel = "Driver inactive"; + } + else { + int count = KbManager::devices().count(); + // Warn if the daemon version doesn't match the GUI + QString daemonWarning; + if(daemonVersion != CKB_VERSION_STR) + daemonWarning = "\n\nWarning: Driver version mismatch (" + daemonVersion + "). Please upgrade ckb" + QString(KbManager::ckbDaemonVersionF() > KbManager::ckbGuiVersionF() ? "" : "-daemon") + ". If the problem persists, try rebooting."; + if(count == 0) + deviceLabel = "No devices connected" + daemonWarning; + else if(count == 1) + deviceLabel = "1 device connected" + daemonWarning; + else + deviceLabel = QString("%1 devices connected").arg(count) + daemonWarning; + } + + qOut() + << "ckb " << CKB_VERSION_STR + << endl + << "Open Source Corsair Input Device Driver for Linux and OSX." + << endl << endl + << deviceLabel + << endl << endl + << "See https://github.com/ccMSC/ckb" + << endl + << QString::fromUtf8("©") << " 2014-2016. Licensed under GPLv2." + << endl; + break; + } + case Command::CommandLayout: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + // further specify the layout command + QString task = commands[cmdOffset++].toLower(); + if (task.compare("list") == 0) { + // get currently set layout and available layouts + KeyMap::Layout layout = KeyMap::getLayout(settings.value("Program/KbdLayout").toString()); + QStringList layoutNames = KeyMap::layoutNames(); + + // iterate through available layouts + for (int layoutIndex = 0; layoutIndex < layoutNames.count(); layoutIndex++) { + // print out all available keyboard layouts + KeyMap::Layout currentLayout = KeyMap::Layout(layoutIndex); + qOut() + << qSetFieldWidth(0) << left << ((layout == currentLayout) ? "[x]" : "[ ]") << " " + << qSetFieldWidth(9) << left << KeyMap::getLayout(currentLayout).toUpper() << " | " + << qSetFieldWidth(0) << left << layoutNames[layoutIndex] + << endl; + } + } + else if (task.compare("set") == 0) { + // get next argument to set as the new layout + if (cmdOffset >= commands.length()) return CommandLineInvalid; + KeyMap::Layout kl = KeyMap::getLayout(commands[cmdOffset++]); + + // if layout is invalid, abort + if (kl == KeyMap::NO_LAYOUT) { + qOut() + << "Could not set layout." + << endl; + + return CommandLineInvalid; + } + else { + // persistently save new layout + settings.set("Program/KbdLayout", KeyMap::getLayout(kl)); + Kb::layout(kl); + + // wait until settings are written completely + settings.cleanUp(); + + qOut() + << "New layout is " << KeyMap::layoutNames()[(int)Kb::layout()] << "." + << endl; + } + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandModifier: + { + // load the global remaps from the ckbsettings + KbBind::loadGlobalRemap(); + + // set the modifier keys and names to display + QStringList modKeys, modNames; + modKeys << "caps" << "lshift" << "lctrl" << "lalt" << "lwin"; +#ifdef Q_OS_MACX + modNames << "Caps Lock" << "Shift" << "Control (⌃)" << "Option (⌥)" << "Command (⌘)"; +#else + modNames << "Caps Lock" << "Shift" << "Control" << "Alt" << "Super"; +#endif + + if (cmdOffset >= commands.length()) return CommandLineInvalid; + QString task = commands[cmdOffset++].toLower(); + if (task.compare("list") == 0) { + // print each modifier and it's rebind + qOut() + << "These will override the keyboard profile. See \"Binding\" tab for more settings." + << endl; + + foreach (QString mod, modKeys) { + qOut() + << qSetFieldWidth(11) << left << modNames[modKeys.indexOf(mod)] + << qSetFieldWidth(2) << left << ": " + << qSetFieldWidth(0) << left << modNames[modKeys.indexOf(KbBind::globalRemap(mod))] + << endl; + } + } + else if (task.compare("set") == 0) { + // abort if arguments don't contain key and remap + if (cmdOffset+1 >= commands.length()) return CommandLineInvalid; + + // get key and remap from arguments + QString key = commands[cmdOffset++].toLower(); + QString mod = commands[cmdOffset++].toLower(); + QString lmod, rmod; + + // strip leading 'r' or 'l' + mod = mod[0] == QChar('l') || mod[0] == QChar('r') ? mod.mid(1) : mod; + + // abort if remap not in list + if ((QStringList() << "caps" << "shift" << "ctrl" << "alt" << "option" << "win" << "cmd").indexOf(mod) == -1) + return CommandLineInvalid; + + // initialize newMods + QHash newMods; + newMods["caps"] = KbBind::globalRemap("caps"); + newMods["lshift"] = KbBind::globalRemap("lshift"); + newMods["rshift"] = KbBind::globalRemap("rshift"); + newMods["lctrl"] = KbBind::globalRemap("lctrl"); + newMods["rctrl"] = KbBind::globalRemap("rctrl"); + newMods["lalt"] = KbBind::globalRemap("lalt"); + newMods["ralt"] = KbBind::globalRemap("ralt"); + newMods["lwin"] = KbBind::globalRemap("lwin"); + newMods["rwin"] = KbBind::globalRemap("rwin"); + + // coerce remaps to understandable format + if (mod == "caps") { + lmod = rmod = mod; + } + else if (mod == "option") { + lmod = "lalt"; + rmod = "ralt"; + } + else if (mod == "cmd") { + lmod = "lwin"; + rmod = "rwin"; + } + else { + lmod = 'l' + mod; + rmod = 'r' + mod; + } + + // remap requested key to new mod + if (key.compare("caps") == 0) { + newMods["caps"] = lmod; + } + else if (key.compare("shift") == 0) { + newMods["lshift"] = lmod; + newMods["rshift"] = rmod; + } + else if (key.compare("ctrl") == 0) { + newMods["lctrl"] = lmod; + newMods["rctrl"] = rmod; + } + else if (key.compare("alt") == 0 || key.compare("option") == 0) { + newMods["lalt"] = lmod; + newMods["ralt"] = rmod; + } + else if (key.compare("win") == 0 || key.compare("cmd") == 0) { + newMods["lwin"] = lmod; + newMods["rwin"] = rmod; + } + else { + return CommandLineInvalid; + } + + // persistently store new remaps + KbBind::setGlobalRemap(newMods); + KbBind::saveGlobalRemap(); + + // wait until settings are written completely + settings.cleanUp(); + } + else if (task.compare("reset") == 0) { + // initialize newMods + QHash newMods; + newMods["caps"] = "caps"; + newMods["lshift"] = "lshift"; + newMods["rshift"] = "rshift"; + newMods["lctrl"] = "lctrl"; + newMods["rctrl"] = "rctrl"; + newMods["lalt"] = "lalt"; + newMods["ralt"] = "ralt"; + newMods["lwin"] = "lwin"; + newMods["rwin"] = "rwin"; + + // persistently store new remaps + KbBind::setGlobalRemap(newMods); + KbBind::saveGlobalRemap(); + + // wait until settings are written completely + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + + break; + } + case Command::CommandFramerate: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + if (task.compare("show") == 0) { + // display current framerate + qOut() + << "Current framerate: " + << settings.value("Program/framerate").toString() + << endl; + } + else if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + // get given framerate + bool ok; + QString frameRate = commands[cmdOffset++].toLower(); + int frameRateValue = frameRate.toInt(&ok); + + // check, whether it's a number between 0 and 60 + if (!ok || frameRateValue <= 0 || frameRateValue >= 60) { + qOut() + << "Framerate must be a number between 0 and 60." + << endl; + return CommandLineInvalid; + } + + // persistently store framerate and set it for current session + Kb::frameRate(frameRateValue); + settings.set("Program/framerate", frameRateValue); + + // print warning, if value above 30 + if (frameRateValue > 30) + qOut() + << "Warning: high frame rates may cause stability issues." + << endl; + + // wait until settings are written completely + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandAnimationDir: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + // whether it's "show" or "scan", both do the same + QString task = commands[cmdOffset++].toLower(); + if (task.compare("show") == 0 || task.compare("scan") == 0) { + // scan and load the animations from the animation-dir + AnimScript::scan(); + + // display information about the results + qOut() + << "Location: " + << AnimScript::path() + << " "; + switch (AnimScript::count()) { + case 0: + qOut() << "No animations found."; + break; + case 1: + qOut() << "1 animation found."; + break; + default: + qOut() << QString("%1 animations found").arg(AnimScript::count()); + break; + } + qOut() << endl; + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandBrightnessPerMode: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // get current dimming from settings + int dimming = settings.value("Program/GlobalBrightness").toInt(); + if (dimming < -1 || dimming > KbLight::MAX_DIM) { + // normalize dimming, if value is malformed + dimming = 0; + } + + // display information about brightness per mode + qOut() + << "Brightness per mode: " + << (dimming == -1 ? "Enabled" : "Disabled") << "." + << endl + << "(By default, the same brightness level will be applied to all profiles and all devices. Enable this to store it with the lighting mode instead.)" + << endl; + } + else if (task.compare("enable") == 0) { + // enable brightness per mode -> disable shared dimming + settings.set("Program/GlobalBrightness", -1); + KbLight::shareDimming(-1); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // disable brightness per mode -> enable shared dimming + settings.set("Program/GlobalBrightness", 0); + KbLight::shareDimming(0); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandSpatialDithering: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about spatial dithering + qOut() + << "Use spatial dithering to simulate extra color resolution: " + << (settings.value("Program/Dither").toBool() ? "Enabled" : "Disabled") << "." + << endl + << "(May improve appearance on some keyboards.)" + << endl; + } + else if (task.compare("enable") == 0) { + // enable spatial dithering + settings.set("Program/Dither", true); + Kb::dither(true); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // disable spatial dithering + settings.set("Program/Dither", false); + Kb::dither(false); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandFirmwareAutocheck: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about firmware auto check + qOut() + << "Check for new firmware automatically: " + << (settings.value("Program/DisableAutoFWCheck").toBool() ? "Disabled" : "Enabled") << "." + << endl + << "(You will be notified when new firmware versions are available. You'll have the option to install them immediately or wait until later.)" + << endl; + } + else if (task.compare("enable") == 0) { + // enable firmware autocheck + settings.set("Program/DisableAutoFWCheck", false); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // disable firmware autocheck + settings.set("Program/DisableAutoFWCheck", true); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandTrayIcon: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about the tray icon + qOut() + << "Show tray icon: " + << (settings.value("Program/SuppressTrayIcon").toBool() ? "Disabled" : "Enabled") << "." + << endl + << "(The tray icon will not be displayed, if disabled. The application will still run in the background; re-launch the app to see the GUI again.)" + << endl; + } + else if (task.compare("enable") == 0) { + // suppress tray icon + settings.set("Program/SuppressTrayIcon", false); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // allow tray icon + settings.set("Program/SuppressTrayIcon", true); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } +#ifdef Q_OS_MACX + case Command::CommandMouseAcceleration: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about mouse acceleration + qOut() + << "Mouse acceleration: " + << (settings.value("Program/DisableMouseAccel").toBool() ? "Disabled" : "Enabled") << "." + << endl + << "(Try this if you're having problems with mouse movement.)" + << endl; + } + else if (task.compare("enable") == 0) { + // suppress mouse acceleration + settings.set("Program/DisableMouseAccel", false); + Kb::mouseAccel(true); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // allow mouse acceleration + settings.set("Program/DisableMouseAccel", true); + Kb::mouseAccel(false); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandScrollAcceleration: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about scroll acceleration + qOut() + << "Scroll acceleration: " + << (settings.value("Program/DisableScrollAccel").toBool() ? "Disabled" : "Enabled") << "." + << endl + << "(Try this if you're having problems with the scroll wheel.)" + << endl; + } + else if (task.compare("enable") == 0) { + // suppress scroll acceleration + settings.set("Program/DisableScrollAccel", false); + Kb::scrollSpeed(settings.value("Program/ScrollSpeed", 3).toInt()); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // allow scroll acceleration + settings.set("Program/DisableScrollAccel", true); + Kb::scrollSpeed(0); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } + case Command::CommandScrollAccelerationSpeed: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + bool scrollDisabled = settings.value("Program/DisableScrollAccel"); + + if (task.compare("show") == 0) { + // display information about scroll acceleration speed + qOut() + << "Scroll acceleration value: " + << settings.value("Program/ScrollSpeed").toInt() << "." + << endl; + if (scrollDisabled) { + qOut() + << "Attention: Scroll Acceleration is disabled. It must be enabled for the Scroll Acceleration to be set to a specific value." + << endl; + break; + } + else if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + // get given scroll speed + bool ok; + QString scrollSpeed = commands[cmdOffset++].toLower(); + int scrollSpeedValue = scrollSpeed.toInt(&ok); + + // check, whether it's a number between 0 and 10 + if (!ok || scrollSpeedValue < 1 || scrollSpeedValue > 10) { + qOut() + << "Scroll speed must be a number between 1 and 10." + << endl; + return CommandLineInvalid; + } + + // set scroll acceleration speed (if enabled) + settings.set("Program/ScrollSpeed", scrollSpeedValue); + Kb::scrollSpeed(scrollDisabled ? 0 : scrollSpeedValue); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + break; + } +#endif + case Command::CommandLongMacroDelay: + { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + QString task = commands[cmdOffset++].toLower(); + + // coerce "set" to "disable"/"enable" if possible + if (task.compare("set") == 0) { + if (cmdOffset >= commands.length()) return CommandLineInvalid; + + task = commands[cmdOffset].toLower(); + if (task.compare("on") == 0 || task.compare("1") == 0) + task = "enable"; + else if (task.compare("off") == 0 || task.compare("0") == 0) + task = "disable"; + else + if (cmdOffset >= commands.length()) return CommandLineInvalid; + } + + if (task.compare("show") == 0) { + // display information about the long macro delay + qOut() + << "Use delay for very long macros: " + << (settings.value("Program/MacroDelay").toBool() ? "Enabled" : "Disabled") << "." + << endl + << "(When using macros with strings longer than 25 chars, some OS may lose characters (e.g. Mint 17.2). Select to prevent that bug.)" + << endl; + } + else if (task.compare("enable") == 0) { + // enable long macro delay + settings.set("Program/MacroDelay", true); + Kb::macroDelay(true); + settings.cleanUp(); + } + else if (task.compare("disable") == 0) { + // disable long macro delay + settings.set("Program/MacroDelay", false); + Kb::macroDelay(false); + settings.cleanUp(); + } + else { + return CommandLineInvalid; + } + + break; + } + case Command::CommandHelp: + return CommandLineInvalid; + default: + return CommandLineUnknown; + } + + return CommandLineOK; +} + +/** + * run - Run specified Commands. + */ +int CommandLine::run() { + cmdOffset = 0; + + // parse commands and execute requested operation + if (cmdOffset >= commands.length()) return CommandLineUnknown; + switch (Command::resolveCommand(commands[cmdOffset++])) { + case Command::CommandGlobal: + { + int global = runGlobal(); + if (global == CommandLineInvalid) { + qOut() + << "Syntax: global CMD" << endl + << "Where: CMD = info" << endl + << " | layout { list | set LAYOUT }" << endl + << " | modifier { list | set KEY MOD | reset }" << endl + << " | framerate { show | set VALUE }" << endl + << " | animation-dir { show | scan }" << endl + << " | brightness-per-mode { show | set VALUE }" << endl + << " | spatial-dithering { show | set VALUE }" << endl + << " | firmware-autocheck { show | set VALUE }" << endl + << " | tray-icon { show | set VALUE }" << endl + << " | mouse-acceleration { show | set VALUE }" << endl + << " | scroll-acceleration { show | set VALUE }" << endl + << " | scroll-acceleration-speed { show set VALUE }" << endl + << " | long-macro-delay { show | set VALUE }" << endl << endl; + + global = CommandLineOK; + } + + return global; + } + case Command::CommandDevice: + qDebug() << "Device:"; + break; + case Command::CommandProfile: + qDebug() << "Profile:"; + break; + default: + return CommandLineUnknown; + } + + // return with appropriate status + return CommandLineOK; +} + + +/** + * execute - Parse arguments and run specified commands. + * + * @param args QStringList of arguments to parse. + */ +int CommandLine::execute(QStringList args) { + CkbSettings settings("Program"); + KbManager::init(CKB_VERSION_STR); + KbManager::kbManager()->scanKeyboards(); + QStringList::const_iterator constIterator; + CommandLine cli; + + // setup command by splitting base, flag and actual command + int index = 0; + QString item; + for (constIterator = args.constBegin(); constIterator != args.constEnd(); ++constIterator) { + item = (*constIterator); + if (index == 0) { + // first arg is the base + cli.base = item; + } + else if (index == 1) { + // second arg is the flag + cli.flag = item; + } + else { + // the remaining args are the command to execute + cli.commands << item; + } + + index++; + } + + // run command + return cli.run(); +} diff --git a/src/ckb/cli.h b/src/ckb/cli.h new file mode 100644 index 0000000..1233305 --- /dev/null +++ b/src/ckb/cli.h @@ -0,0 +1,71 @@ +#ifndef CKB_CLI_H +#define CKB_CLI_H + +/* Include Section */ +#include +#include +#include "ckbsettings.h" + + +class Command : public QObject +{ + Q_OBJECT +public: + enum CommandType { + // Top Level Commands + CommandGlobal, + CommandDevice, + CommandProfile, + + // Sub-commands + CommandHelp, + CommandInfo, + CommandLayout, + CommandModifier, + CommandFramerate, + CommandAnimationDir, + CommandBrightnessPerMode, + CommandSpatialDithering, + CommandFirmwareAutocheck, + CommandTrayIcon, + CommandMouseAcceleration, + CommandScrollAcceleration, + CommandScrollAccelerationSpeed, + CommandLongMacroDelay, + + // Generic OK/Fail Flags + CommandOK, + CommandUnknown + }; + + static int resolveCommand(QString cmd); +}; + + +class CommandLine : public QObject +{ + Q_OBJECT +private: + CkbSettings settings; + int cmdOffset; + + int runGlobal(); + int run(); +public: + QString base; + QString flag; + QStringList commands; + + enum CommandLineResult { + CommandLineOK, + CommandLineInvalid, + CommandLineUnknown + }; + static int execute(QStringList args); + /** static stdout stream to print text similar to qDebug() */ + inline QTextStream& qOut() { static QTextStream s{stdout}; return s; } + + friend class Command; +}; + +#endif // CKB_CLI_H diff --git a/src/ckb/kbmanager.h b/src/ckb/kbmanager.h index 9935cf5..4330e86 100644 --- a/src/ckb/kbmanager.h +++ b/src/ckb/kbmanager.h @@ -42,6 +42,8 @@ class KbManager : public QObject // Timer for scanning the driver/device list. May also be useful for periodic GUI events. Created during init(), always runs at 10FPS. static inline QTimer* scanTimer() { return _kbManager ? _kbManager->_scanTimer : 0; } + friend class CommandLine; + signals: // A new device was connected. void kbConnected(Kb* device); diff --git a/src/ckb/main.cpp b/src/ckb/main.cpp index cf78909..5ca860c 100644 --- a/src/ckb/main.cpp +++ b/src/ckb/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "cli.h" QSharedMemory appShare("ckb"); @@ -27,7 +28,8 @@ enum CommandLineParseResults { CommandLineVersionRequested, CommandLineHelpRequested, CommandLineClose, - CommandLineBackground + CommandLineBackground, + CommandLineCommand }; /** @@ -55,6 +57,10 @@ CommandLineParseResults parseCommandLine(QCommandLineParser &parser, QString *er const QCommandLineOption closeOption(QStringList() << "c" << "close", "Causes already running instance (if any) to exit."); parser.addOption(closeOption); + // add -x, --cmd, --execute + const QCommandLineOption commandOption(QStringList() << "x" << "cmd" << "execute", + "Execute the given command without a GUI."); + parser.addOption(commandOption); /* parse arguments */ if (!parser.parse(QCoreApplication::arguments())) { @@ -84,6 +90,11 @@ CommandLineParseResults parseCommandLine(QCommandLineParser &parser, QString *er return CommandLineClose; } + if (parser.isSet(commandOption)) { + // parse input and execute appropriate command + return CommandLineCommand; + } + /* no explicit argument was passed */ return CommandLineOK; }; @@ -159,6 +170,7 @@ int main(int argc, char *argv[]){ QString errorMessage; parser.setApplicationDescription("Open Source Corsair Input Device Driver for Linux and OSX."); bool background = 0; + bool isCmd = 0; // Although the daemon runs as root, the GUI needn't and shouldn't be, as it has the potential to corrupt settings data. if(getuid() == 0){ @@ -189,7 +201,7 @@ int main(int argc, char *argv[]){ case CommandLineVersionRequested: // If launched with --version, print version info and then quit printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), - qPrintable(QCoreApplication::applicationVersion())); + qPrintable(QCoreApplication::applicationVersion())); return 0; case CommandLineHelpRequested: // If launched with --help, print help and then quit @@ -202,6 +214,17 @@ int main(int argc, char *argv[]){ else printf("ckb is not running.\n"); return 0; + case CommandLineCommand: + // If launched with --cmd, try to execute given command + static int cli_error = CommandLine::execute(QCoreApplication::arguments()); + if (cli_error) { + // display help text and errors, if command couldn't be executed + if (cli_error == CommandLine::CommandLineUnknown) + parser.showHelp(); + return cli_error; + } + // Also run ckb in background on execute command + isCmd = 1; case CommandLineBackground: // If launched with --background, launch in background background = 1; @@ -216,7 +239,7 @@ int main(int argc, char *argv[]){ #endif if(isRunning(background ? 0 : "Open")){ - printf("ckb is already running. Exiting.\n"); + if (!isCmd) printf("ckb is already running. Exiting.\n"); return 0; } MainWindow w;