#include "automation_protocol.h" #include #include #include #include #include namespace crossdesk::cli::automation { void Fields(const json &j, std::initializer_list names) { if (!j.is_object()) throw std::invalid_argument("INVALID_REQUEST"); for (auto it = j.begin(); it != j.end(); ++it) if (std::none_of(names.begin(), names.end(), [&](const char *s) { return it.key() == s; })) throw std::invalid_argument("UNKNOWN_FIELD"); } int Number(const json &j, const char *key, int low, int high, int fallback) { if (!j.contains(key)) return fallback; if (!j[key].is_number_integer()) throw std::invalid_argument("INVALID_REQUEST"); const auto n = j[key].get(); if (n < low || n > high) throw std::invalid_argument("INVALID_REQUEST"); return int(n); } uint64_t Sequence(const json &j, const char *key, uint64_t fallback) { if (!j.contains(key)) return fallback; if (!j[key].is_number_unsigned()) throw std::invalid_argument("INVALID_REQUEST"); return j[key].get(); } bool Flag(const json &j, const char *key, bool fallback) { if (!j.contains(key)) return fallback; if (!j[key].is_boolean()) throw std::invalid_argument("INVALID_REQUEST"); return j[key].get(); } std::string String(const json &j, const char *key, size_t limit) { if (!j.contains(key) || !j[key].is_string()) throw std::invalid_argument("INVALID_REQUEST"); auto s = j[key].get(); if (s.empty() || s.size() > limit || s.find('\0') != std::string::npos) throw std::invalid_argument("INVALID_REQUEST"); return s; } namespace { int KeyCode(const std::string &key) { if (key.size() == 1 && ((key[0] >= 'A' && key[0] <= 'Z') || (key[0] >= '0' && key[0] <= '9'))) return key[0]; static const std::map keys = {{"BACKSPACE", 8}, {"TAB", 9}, {"ENTER", 13}, {"SHIFT", 16}, {"CTRL", 17}, {"ALT", 18}, {"PAUSE", 19}, {"CAPS_LOCK", 20}, {"ESC", 27}, {"SPACE", 32}, {"PAGE_UP", 33}, {"PAGE_DOWN", 34}, {"END", 35}, {"HOME", 36}, {"LEFT", 37}, {"UP", 38}, {"RIGHT", 39}, {"DOWN", 40}, {"INSERT", 45}, {"DELETE", 46}, {"META", 91}, {"F1", 112}, {"F2", 113}, {"F3", 114}, {"F4", 115}, {"F5", 116}, {"F6", 117}, {"F7", 118}, {"F8", 119}, {"F9", 120}, {"F10", 121}, {"F11", 122}, {"F12", 123}, {"SEMICOLON", 186}, {"EQUAL", 187}, {"COMMA", 188}, {"MINUS", 189}, {"PERIOD", 190}, {"SLASH", 191}, {"BACKQUOTE", 192}, {"BRACKET_LEFT", 219}, {"BACKSLASH", 220}, {"BRACKET_RIGHT", 221}, {"QUOTE", 222}}; auto it = keys.find(key); if (it == keys.end()) throw std::invalid_argument("INVALID_KEY"); return it->second; } } // namespace std::vector BuildInputSteps(const json &a, const InputGeometry &o) { const auto type = String(a, "type", 32); std::vector steps; auto point = [&](const json &p, int delay, int flag = 0, int wheel = 0) { auto coordinate = [&](const char *key, int size, int offset, int span, int source) { if (!p.contains(key) || !p[key].is_number()) throw std::invalid_argument("INVALID_COORDINATE"); const auto value = p[key].get(); if (!std::isfinite(value) || value < 0 || value >= size) throw std::invalid_argument("INVALID_COORDINATE"); const auto center = std::min(value + .5, double(size) - .5); return float(std::clamp((offset + center * span / size) / source, 0.0, 1.0)); }; Step step; step.delay = delay; step.flag = flag; step.wheel = wheel; step.x = coordinate("x", o.width, o.region.x, o.region.width, o.source_width); step.y = coordinate("y", o.height, o.region.y, o.region.height, o.source_height); steps.push_back(std::move(step)); }; auto key = [&](int code, bool down, int delay) { Step step; step.kind = StepKind::Key; step.key = code; step.down = down; step.delay = delay; steps.push_back(std::move(step)); }; if (type == "move" || type == "click" || type == "double_click") { Fields(a, type == "move" ? std::initializer_list{"type", "x", "y", "modifiers"} : std::initializer_list{"type", "x", "y", "button", "modifiers"}); point(a, 0); if (type != "move") { const auto button = a.contains("button") ? String(a, "button", 8) : "left"; const int flag = button == "left" ? 1 : button == "right" ? 3 : button == "middle" ? 5 : -1; if (flag < 0) throw std::invalid_argument("INVALID_BUTTON"); point(a, 10, flag); point(a, 40, flag + 1); if (type == "double_click") { point(a, 110, flag); point(a, 140, flag + 1); } } } else if (type == "scroll") { Fields(a, {"type", "x", "y", "direction", "ticks", "modifiers"}); const auto direction = String(a, "direction", 8); if (direction != "up" && direction != "down" && direction != "left" && direction != "right") throw std::invalid_argument("INVALID_DIRECTION"); const int ticks = Number(a, "ticks", 1, 10, 1); point(a, 0); point(a, 10, direction == "left" || direction == "right" ? 8 : 7, (direction == "up" || direction == "right" ? 120 : -120) * ticks); } else if (type == "drag") { Fields(a, {"type", "path", "duration_ms", "button", "modifiers"}); const auto button = a.contains("button") ? String(a, "button", 8) : "left"; const int flag = button == "left" ? 1 : button == "right" ? 3 : button == "middle" ? 5 : -1; if (flag < 0) throw std::invalid_argument("INVALID_BUTTON"); const auto &path = a.at("path"); if (!path.is_array() || path.size() < 2 || path.size() > 128) throw std::invalid_argument("INVALID_PATH"); const int duration = Number(a, "duration_ms", 100, 2000, 500); for (const auto &p : path) Fields(p, {"x", "y"}); point(path.front(), 0); point(path.front(), 10, flag); for (size_t i = 1; i < path.size(); ++i) point(path[i], 10 + int(i * duration / (path.size() - 1))); point(path.back(), duration + 30, flag + 1); } else if (type == "key_press") { Fields(a, {"type", "key", "modifiers"}); const auto code = KeyCode(String(a, "key", 16)); key(code, true, 0); key(code, false, 40); } else if (type == "hotkey") { Fields(a, {"type", "keys"}); const auto &keys = a.at("keys"); if (!keys.is_array() || keys.size() < 2 || keys.size() > 6) throw std::invalid_argument("INVALID_KEYS"); std::set seen; std::vector codes; for (const auto &item : keys) { if (!item.is_string()) throw std::invalid_argument("INVALID_KEY"); int code = KeyCode(item.get()); if (!seen.insert(code).second) throw std::invalid_argument("DUPLICATE_KEY"); codes.push_back(code); } int delay = 0; for (int code : codes) { key(code, true, delay); delay += 15; } delay += 30; for (auto it = codes.rbegin(); it != codes.rend(); ++it) { key(*it, false, delay); delay += 15; } } else if (type == "type_text") { Fields(a, {"type", "text"}); Step step; step.kind = StepKind::Text; step.text = String(a, "text", 16384); steps.push_back(std::move(step)); } else throw std::invalid_argument("UNSUPPORTED_ACTION"); if (a.contains("modifiers")) { const auto &modifiers = a["modifiers"]; if (!modifiers.is_array() || modifiers.size() > 4) throw std::invalid_argument("INVALID_KEYS"); std::set seen; std::vector prefix; for (const auto &item : modifiers) { if (!item.is_string()) throw std::invalid_argument("INVALID_KEY"); const auto name = item.get(); if (name != "CTRL" && name != "ALT" && name != "SHIFT" && name != "META") throw std::invalid_argument("INVALID_KEY"); const int code = KeyCode(name); if (!seen.insert(code).second || (type == "key_press" && String(a, "key", 16) == name)) throw std::invalid_argument("DUPLICATE_KEY"); Step step; step.kind = StepKind::Key; step.key = code; step.down = true; step.delay = int(prefix.size()) * 15; prefix.push_back(step); } const int offset = int(prefix.size()) * 15; for (auto &step : steps) step.delay += offset; steps.insert(steps.begin(), prefix.begin(), prefix.end()); int delay = steps.back().delay + 15; for (auto it = prefix.rbegin(); it != prefix.rend(); ++it, delay += 15) key(it->key, false, delay); } return steps; } } // namespace crossdesk::cli::automation