// `alvin` - command-line front end to the library, handy for trying backends // and scripting simple automations. #include #include #include #include #include #include #include #include #include "alvin/alvin.hpp" #ifdef ALVIN_CLI_VISION #include "alvin/vision.hpp" #endif namespace fs = std::filesystem; using namespace alvin; namespace { void usage() { std::cerr << R"(usage: alvin [--input wlr|libei|uinput|sendinput] [args] monitors list monitors (logical desktop coordinates) windows list top-level windows active show the focused window focus focus the first window whose title/class contains move move the pointer to desktop coordinates moveby move the pointer relatively click [left|right|middle] [n] click n times (default: left, 1) clickat [button] move, then click scroll [dx] scroll wheel notches (positive = down/right) key press a chord, e.g. ctrl+shift+t type type UTF-8 text capture [--window] [--cursor] save one frame; the portal token is cached so the picker only appears the first time )" #ifdef ALVIN_CLI_VISION R"( find [threshold] locate an image on screen, print desktop coords )" #endif ; } int report(const Error& e) { std::cerr << "alvin: " << toString(e.code) << ": " << e.message << '\n'; return 1; } int report(const Status& s) { return s ? 0 : report(s.error()); } std::optional toInt(const std::string& s) { try { std::size_t used = 0; int v = std::stoi(s, &used); if (used == s.size()) return v; } catch (...) { } return std::nullopt; } std::optional toButton(const std::string& s) { if (s == "left") return MouseButton::Left; if (s == "right") return MouseButton::Right; if (s == "middle") return MouseButton::Middle; if (s == "back") return MouseButton::Back; if (s == "forward") return MouseButton::Forward; return std::nullopt; } fs::path tokenFile(const char* name) { const char* cache = std::getenv("XDG_CACHE_HOME"); const char* home = std::getenv("HOME"); fs::path dir = cache && *cache ? fs::path(cache) : fs::path(home ? home : ".") / ".cache"; return dir / "alvin" / name; } std::string readToken(const fs::path& p) { std::ifstream f(p); std::string t; std::getline(f, t); return t; } void writeToken(const fs::path& p, const std::string& token) { if (token.empty()) return; std::error_code ec; fs::create_directories(p.parent_path(), ec); std::ofstream(p) << token << '\n'; } void printWindow(const WindowInfo& w) { std::printf("%#llx %5d,%-5d %5dx%-5d %s%s[%s] %s\n", static_cast(w.id.value), w.bounds.x, w.bounds.y, w.bounds.width, w.bounds.height, w.focused ? "* " : " ", w.minimized ? "(min) " : "", w.appId.c_str(), w.title.c_str()); } Result> makeInput(const InputOptions& base) { InputOptions opts = base; fs::path tf = tokenFile("remotedesktop.token"); opts.restoreToken = readToken(tf); auto in = Input::create(opts); if (in) { writeToken(tf, (*in)->restoreToken()); if (std::getenv("ALVIN_VERBOSE")) std::cerr << "input backend: " << toString((*in)->backend()) << '\n'; } return in; } Result captureFrame(bool window, bool cursor) { CaptureOptions opts; opts.source = window ? CaptureSource::Window : CaptureSource::Monitor; opts.showCursor = cursor; fs::path tf = tokenFile(window ? "screencast-window.token" : "screencast-monitor.token"); opts.restoreToken = readToken(tf); auto cap = Capture::create(opts); if (!cap) return std::unexpected(cap.error()); writeToken(tf, (*cap)->restoreToken()); return (*cap)->grab(); } int run(std::span args, const InputOptions& inputOpts) { const std::string& cmd = args[0]; auto arg = [&](std::size_t i) -> const std::string& { static const std::string empty; return i < args.size() ? args[i] : empty; }; if (cmd == "monitors") { auto mons = listMonitors(); if (!mons) return report(mons.error()); for (const auto& m : *mons) std::printf("%-10s %5d,%-5d %5dx%-5d pixels %dx%d scale %.3g%s %s\n", m.name.c_str(), m.bounds.x, m.bounds.y, m.bounds.width, m.bounds.height, m.pixelSize.width, m.pixelSize.height, m.scale, m.primary ? " (primary)" : "", m.description.c_str()); return 0; } if (cmd == "windows" || cmd == "active" || cmd == "focus") { auto wm = WindowManager::create(); if (!wm) return report(wm.error()); if (cmd == "windows") { auto list = (*wm)->list(); if (!list) return report(list.error()); for (const auto& w : *list) printWindow(w); return 0; } if (cmd == "active") { auto a = (*wm)->active(); if (!a) return report(a.error()); if (*a) printWindow(**a); return 0; } auto w = (*wm)->find(arg(1)); if (!w) return report(w.error()); if (!*w) return report(Error{ErrorCode::NotFound, "no window matches '" + arg(1) + "'"}); return report((*wm)->focus((*w)->id)); } if (cmd == "capture") { if (arg(1).empty()) return usage(), 2; bool window = false, cursor = false; for (std::size_t i = 2; i < args.size(); ++i) { if (args[i] == "--window") window = true; else if (args[i] == "--cursor") cursor = true; } auto frame = captureFrame(window, cursor); if (!frame) return report(frame.error()); fs::path out = arg(1); Status saved; #ifdef ALVIN_CLI_VISION if (out.extension() != ".ppm") saved = vision::saveImage(*frame, out); else #endif saved = frame->savePpm(out); if (!saved) return report(saved); Rect r = frame->desktopRect(); std::printf("%s: %dx%d pixels, desktop rect %d,%d %dx%d\n", out.c_str(), frame->width(), frame->height(), r.x, r.y, r.width, r.height); return 0; } #ifdef ALVIN_CLI_VISION if (cmd == "find") { auto tpl = vision::loadImage(arg(1)); if (!tpl) return report(tpl.error()); vision::MatchOptions mo; if (!arg(2).empty()) mo.threshold = std::stod(arg(2)); auto frame = captureFrame(false, false); if (!frame) return report(frame.error()); auto match = vision::find(*frame, *tpl, mo); if (!match) return report(match.error()); if (!*match) { std::printf("not found\n"); return 1; } Point c = frame->toDesktop((*match)->rect.center()); std::printf("%d %d (score %.3f)\n", c.x, c.y, (*match)->score); return 0; } #endif // Everything below needs an input backend. auto input = makeInput(inputOpts); if (!input) return report(input.error()); Input& in = **input; if (cmd == "move" || cmd == "moveby" || cmd == "clickat") { auto x = toInt(arg(1)), y = toInt(arg(2)); if (!x || !y) return usage(), 2; if (cmd == "moveby") return report(in.moveBy(*x, *y)); if (cmd == "move") return report(in.moveTo({*x, *y})); auto b = arg(3).empty() ? std::optional{MouseButton::Left} : toButton(arg(3)); if (!b) return usage(), 2; return report(in.clickAt({*x, *y}, *b)); } if (cmd == "click") { auto b = arg(1).empty() ? std::optional{MouseButton::Left} : toButton(arg(1)); auto n = arg(2).empty() ? std::optional{1} : toInt(arg(2)); if (!b || !n) return usage(), 2; return report(in.click(*b, *n)); } if (cmd == "scroll") { auto dy = toInt(arg(1)); auto dx = arg(2).empty() ? std::optional{0} : toInt(arg(2)); if (!dy || !dx) return usage(), 2; return report(in.scroll(*dx, *dy)); } if (cmd == "key") return report(in.chord(std::string_view(arg(1)))); if (cmd == "type") return report(in.typeText(arg(1))); usage(); return 2; } } // namespace int main(int argc, char** argv) { std::vector args(argv + 1, argv + argc); InputOptions inputOpts; if (args.size() >= 2 && args[0] == "--input") { const std::string& b = args[1]; if (b == "wlr") inputOpts.backend = InputBackend::WlrVirtual; else if (b == "libei") inputOpts.backend = InputBackend::LibEi; else if (b == "uinput") inputOpts.backend = InputBackend::UInput; else if (b == "sendinput") inputOpts.backend = InputBackend::SendInput; else return usage(), 2; args.erase(args.begin(), args.begin() + 2); } if (args.empty() || args[0] == "-h" || args[0] == "--help") { usage(); return args.empty() ? 2 : 0; } return run(args, inputOpts); }