+static std::vector<std::string> split(std::string s)
+{
+ std::vector<std::string> v;
+ std::string tmp;
+
+ for (std::size_t i = 0; i < s.length(); i++)
+ {
+ char c = s[i];
+ if (std::isspace(c))
+ {
+ if (tmp != "")
+ {
+ v.push_back(tmp);
+ tmp = "";
+ }
+ }
+ else
+ {
+ tmp += c;
+ }
+ }
+ if (tmp != "")
+ {
+ v.push_back(tmp);
+ }
+ return v;
+}
+
+
+/* Using readline callback functions means that we need to pass
+ * information from our class to the callback, and the only way
+ * to do so is using globals.
+ */
+
+// Taken from https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/utils.cpp
+static std::string longest_common_prefix(std::string s,
+ const std::vector<std::string>& candidates) {
+ assert(candidates.size() > 0);
+ if (candidates.size() == 1) {
+ return candidates[0];
+ }
+
+ std::string prefix(s);
+ while (true) {
+ // Each iteration of this loop advances to the next location in all the
+ // candidates and sees if they match up to it.
+ size_t nextloc = prefix.size();
+ auto i = candidates.begin();
+ if (i->size() <= nextloc) {
+ return prefix;
+ }
+ char nextchar = (*(i++))[nextloc];
+ for (; i != candidates.end(); ++i) {
+ if (i->size() <= nextloc || (*i)[nextloc] != nextchar) {
+ // Bail out if there's a mismatch for this candidate.
+ return prefix;
+ }
+ }
+ // All candidates have contents[nextloc] == nextchar, so we can safely
+ // extend the prefix.
+ prefix.append(1, nextchar);
+ }
+
+ assert(0 && "unreachable");
+}
+
+std::vector<std::string> commands =
+{
+ "help", "motion", "expression", "set", "clear"
+};
+
+std::vector<std::string> live2dParams =
+{ // https://docs.live2d.com/cubism-editor-manual/standard-parametor-list/?locale=ja
+ "ParamAngleX", "ParamAngleY", "ParamAngleZ",
+ "ParamEyeLOpen", "ParamEyeLSmile", "ParamEyeROpen", "ParamEyeRSmile",
+ "ParamEyeBallX", "ParamEyeBallY", "ParamEyeBallForm",
+ "ParamBrowLY", "ParamBrowRY", "ParamBrowLX", "ParamBrowRX",
+ "ParamBrowLAngle", "ParamBrowRAngle", "ParamBrowLForm", "ParamBrowRForm",
+ "ParamMouthForm", "ParamMouthOpenY", "ParamTere",
+ "ParamBodyAngleX", "ParamBodyAngleY", "ParamBodyAngleZ", "ParamBreath",
+ "ParamArmLA", "ParamArmRA", "ParamArmLB", "ParamArmRB",
+ "ParamHandL", "ParamHandR",
+ "ParamHairFront", "ParamHairSide", "ParamHairBack", "ParamHairFluffy",
+ "ParamShoulderY", "ParamBustX", "ParamBustY",
+ "ParamBaseX", "ParamBaseY"
+};
+std::vector<std::pair<std::string, int> > MCT_motions;
+std::vector<std::string> MCT_expressions;
+std::map<std::string, double> *MCT_overrideMap;
+
+static char **cliCompletionFunction(const char *textCStr, int start, int end)
+{
+ // Reference: https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/readline-complete-subcommand.cpp
+ rl_attempted_completion_over = 1;
+
+ std::string line(rl_line_buffer);
+
+ std::vector<std::string> cmdline = split(line);
+
+ std::vector<std::string> constructed;
+ std::vector<std::string> *vocab = nullptr;
+
+ if (cmdline.size() == 0 ||
+ (cmdline.size() == 1 && line.back() != ' ') ||
+ cmdline[0] == "help")
+ {
+ vocab = &commands;
+ }
+ else if (cmdline[0] == "motion")
+ {
+ for (auto it = MCT_motions.begin(); it != MCT_motions.end(); ++it)
+ {
+ if ((cmdline.size() == 1 && line.back() == ' ') ||
+ (cmdline.size() == 2 && line.back() != ' '))
+ { // motionGroup
+ {
+ constructed.push_back(it->first);
+ }
+ }
+ else if ((cmdline.size() == 2 && line.back() == ' ') ||
+ (cmdline.size() == 3 && line.back() != ' '))
+ { // motionNumber
+ if (it->first == cmdline[1])
+ {
+ for (int i = 0; i < it->second; i++)
+ {
+ constructed.push_back(std::to_string(i));
+ }
+ break;
+ }
+ }
+ else if (cmdline.size() <= 4)
+ { // priority
+ for (int i = 0; i < 4; i++)
+ {
+ constructed.push_back(std::to_string(i));
+ }
+ }
+ }
+
+ vocab = &constructed;
+ }
+ else if (cmdline[0] == "expression")
+ {
+ vocab = &MCT_expressions;
+ }
+ else if (cmdline[0] == "set")
+ {
+ if ((cmdline.size() % 2 == 0 && line.back() != ' ') ||
+ (cmdline.size() % 2 == 1 && line.back() == ' '))
+ {
+ vocab = &live2dParams;
+ }
+ }
+ else if (cmdline[0] == "clear")
+ {
+ constructed.push_back("all");
+ for (auto const &entry : *MCT_overrideMap)
+ {
+ constructed.push_back(entry.first);
+ }
+ vocab = &constructed;
+ }
+
+ if (!vocab)
+ {
+ return nullptr;
+ }
+
+ std::string text(textCStr);
+ std::vector<std::string> matches;
+ std::copy_if(vocab->begin(), vocab->end(), std::back_inserter(matches),
+ [&text](const std::string &s)
+ {
+ return (s.size() >= text.size() &&
+ s.compare(0, text.size(), text) == 0);
+ });
+
+ if (matches.empty())
+ {
+ return nullptr;
+ }
+
+ char** array =
+ static_cast<char**>(malloc((2 + matches.size()) * sizeof(*array)));
+ array[0] = strdup(longest_common_prefix(text, matches).c_str());
+ size_t ptr = 1;
+ for (const auto& m : matches) {
+ array[ptr++] = strdup(m.c_str());
+ }
+ array[ptr] = nullptr;
+ return array;
+}
+
+MouseCursorTracker::MouseCursorTracker(std::string cfgPath,
+ std::vector<std::pair<std::string, int> > motions,
+ std::vector<std::string> expressions)