diff --git a/.gitignore b/.gitignore index cd1fef0..cacf5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,28 @@ pages/ __pycache__/ *.pyc + +# Build artifacts +*.o +*.so +*.a +build/ +Makefile +.qmake.stash + +# Compiled binaries +xovi-ext/uinject/uinject +xovi-ext/grimoire-injector/uinject +xovi-ext/grimoire-injector/grimoire-injector.so + +# Cloned repos (reference only) +xovi-ext/xovi/ +xovi-ext/qmldiff/ +xovi-ext/clipboard-injector/ + +# Analysis binaries +analysis/xochitl +analysis/hashtab + +# Frida scripts (temporary) +frida-explore.js diff --git a/grimoire.py b/grimoire.py index 7b151ab..d4865c1 100644 --- a/grimoire.py +++ b/grimoire.py @@ -94,7 +94,7 @@ def _parse_d(d: str) -> list[list[tuple[float, float]]]: _FONT_CACHE: Optional[tuple[dict, dict[str, float]]] = None DEFAULT_SCALE = 0.07 DEFAULT_X = -550.0 -DEFAULT_Y = 850.0 +DEFAULT_Y = 200.0 LINE_HEIGHT = 1.4 MAX_LINE_WIDTH = 1100 diff --git a/xovi-ext/grimoire-injector/GrimoireInjector.cpp b/xovi-ext/grimoire-injector/GrimoireInjector.cpp index 12c2673..d0623e0 100644 --- a/xovi-ext/grimoire-injector/GrimoireInjector.cpp +++ b/xovi-ext/grimoire-injector/GrimoireInjector.cpp @@ -11,15 +11,32 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include #include #include #include static GrimoireInjector *g_instance = nullptr; +/* Provided by entry.c hooks */ +extern "C" { + void *grimoire_getQmlEngine(void); + void *grimoire_getSceneController(void); + int grimoire_checkReload(void); + void *grimoire_getFramebuffer(int *w, int *h, int *bpl, int *fmt); +} + static void *watchThreadFunc(void *) { fprintf(stderr, "[grimoire] Watch thread started\n"); sleep(5); // Wait for xochitl to fully initialize @@ -28,6 +45,12 @@ static void *watchThreadFunc(void *) { long long lastMod = 0; while (true) { + /* Check for hot-reload signal */ + if (grimoire_checkReload()) { + fprintf(stderr, "[grimoire] Hot-reload signal received! Re-scanning...\n"); + lastMod = 0; // Force re-read of stroke file + } + struct stat st; if (stat(path, &st) == 0 && st.st_mtime != lastMod) { lastMod = st.st_mtime; @@ -47,7 +70,6 @@ GrimoireInjector::GrimoireInjector(QObject *parent) : QObject(parent) { g_instance = this; - // Spawn a real pthread for file watching — no Qt event loop dependency pthread_t tid; pthread_create(&tid, nullptr, watchThreadFunc, nullptr); pthread_detach(tid); @@ -55,96 +77,162 @@ GrimoireInjector::GrimoireInjector(QObject *parent) } void GrimoireInjector::loadAndInject() { - fprintf(stderr, "[grimoire] loadAndInject called on main thread\n"); + QFileInfo fi(m_watchPath); + if (!fi.exists()) return; + + qint64 modTime = fi.lastModified().toMSecsSinceEpoch(); + if (modTime == m_lastModTime) return; + + m_lastModTime = modTime; + fprintf(stderr, "[grimoire] File changed, injecting via synthetic tablet events...\n"); + int count = loadStrokes(m_watchPath); - if (count > 0) { - if (!m_vtableReady) setupVtable(); - injectToClipboard(); + if (count == 0) return; + + /* Find the focused window to post events to */ + QWindow *targetWin = nullptr; + QObject *inputTarget = nullptr; + auto *guiApp = qobject_cast(QCoreApplication::instance()); + if (guiApp) { + targetWin = guiApp->focusWindow(); + inputTarget = guiApp->focusObject(); + if (inputTarget) { + fprintf(stderr, "[grimoire] Focus object: %s @ %p\n", + inputTarget->metaObject()->className(), (void*)inputTarget); + } + + if (!targetWin) { + for (QWindow *win : guiApp->topLevelWindows()) { + if (win->isVisible()) { + targetWin = win; + break; + } + } + } + + /* Check QQuickWindow properties for contentItem/activeFocusItem */ + if (targetWin) { + const QMetaObject *mo = targetWin->metaObject(); + for (int i = mo->propertyOffset(); i < mo->propertyCount(); i++) { + QMetaProperty prop = mo->property(i); + if (strcmp(prop.name(), "contentItem") == 0 || + strcmp(prop.name(), "activeFocusItem") == 0) { + QVariant val = prop.read(targetWin); + QObject *obj = val.value(); + if (obj) { + fprintf(stderr, "[grimoire] %s: %s @ %p\n", + prop.name(), obj->metaObject()->className(), (void*)obj); + if (!inputTarget) inputTarget = obj; + } + } + } + } } -} -bool GrimoireInjector::injectToClipboard() { - if (m_items.empty()) { - fprintf(stderr, "[grimoire] injectToClipboard: no items\n"); - return false; + if (!inputTarget && targetWin) { + inputTarget = targetWin; } - - // Find the QQmlEngine via the application's root objects - QQmlEngine *engine = nullptr; - auto *app = qobject_cast(QCoreApplication::instance()); - if (!app) { - fprintf(stderr, "[grimoire] No QGuiApplication\n"); - return false; + + if (!inputTarget) { + fprintf(stderr, "[grimoire] No input target found\n"); + return; } - // Try to find engine from top-level windows' QML engines - // The clipboard model is typically a context property on the root context - // We'll try to access it via the first available engine - QList engines; - - // Walk through all QObjects to find QQmlEngine - // Simpler: use QQmlEngine::contextForObject on any known QML object - // For now, try to find it via the global app properties - - // Alternative approach: directly manipulate the clipboard via - // xochitl's internal Clipboard model. The model is registered as - // a context property "Clipboard" in the root QML context. - // We need to find the engine first. - - // Try iterating over top-level objects to find a QML-created object - for (QObject *obj : app->children()) { - QQmlContext *ctx = QQmlEngine::contextForObject(obj); - if (ctx) { - engine = ctx->engine(); + fprintf(stderr, "[grimoire] Posting %d strokes as tablet events to %s @ %p\n", + count, inputTarget->metaObject()->className(), (void*)inputTarget); + + /* Find the pen/stylus pointing device */ + const QPointingDevice *penDevice = nullptr; + QList allDevices = QInputDevice::devices(); + for (const QInputDevice *dev : allDevices) { + const auto *pDev = dynamic_cast(dev); + if (!pDev) continue; + fprintf(stderr, "[grimoire] device: %s type=%d pointer=%d\n", + pDev->name().toUtf8().constData(), + (int)pDev->type(), (int)pDev->pointerType()); + if (pDev->pointerType() == QPointingDevice::PointerType::Pen) { + penDevice = pDev; break; } } - - if (!engine) { - // Fallback: try to get engine from any QQuickWindow - fprintf(stderr, "[grimoire] Could not find QQmlEngine from app children\n"); - // Try another approach - enumerate all objects - for (QObject *obj : QObjectList()) { - QQmlContext *ctx = QQmlEngine::contextForObject(obj); - if (ctx) { - engine = ctx->engine(); + if (!penDevice) { + /* Fallback: use first pointing device */ + for (const QInputDevice *dev : allDevices) { + const auto *pDev = dynamic_cast(dev); + if (pDev) { + penDevice = pDev; + fprintf(stderr, "[grimoire] No pen device, using fallback: %s\n", + pDev->name().toUtf8().constData()); break; } } } - - if (!engine) { - fprintf(stderr, "[grimoire] No QQmlEngine found, cannot inject\n"); - return false; + if (!penDevice) { + fprintf(stderr, "[grimoire] No pointing devices available\n"); + return; } - fprintf(stderr, "[grimoire] Found QQmlEngine at %p\n", (void*)engine); + /* Post each stroke as a sequence of tablet events: press → move... → release */ + /* Transform from .rm v6 coords (origin top-center) to screen pixels (origin top-left) */ + /* Screen: 1404x1872. RM v6: x[-702,701] y[0,1871] → screen x = rm_x + 702, y = rm_y */ + const float xOff = 702.0f; + const float yOff = 0.0f; + + for (int s = 0; s < m_items.size(); s++) { + auto *lineItem = reinterpret_cast(m_items[s].get()); + const Line &line = lineItem->line; + + if (line.points.isEmpty()) continue; + + /* Tablet press at first point */ + const auto &first = line.points.first(); + QPointF pos(first.x + xOff, first.y + yOff); + + QTabletEvent press( + QEvent::TabletPress, penDevice, + pos, pos, + first.pressure / 255.0, + 0, 0, 0, 0, 0, + Qt::NoModifier, + Qt::LeftButton, + Qt::LeftButton + ); + QCoreApplication::sendEvent(inputTarget, &press); + + /* Tablet move for intermediate points */ + for (int i = 1; i < line.points.size() - 1; i++) { + const auto &pt = line.points[i]; + QPointF ppos(pt.x + xOff, pt.y + yOff); + + QTabletEvent move( + QEvent::TabletMove, penDevice, + ppos, ppos, + pt.pressure / 255.0, + 0, 0, 0, 0, 0, + Qt::NoModifier, + Qt::NoButton, + Qt::LeftButton + ); + QCoreApplication::sendEvent(inputTarget, &move); + } - // Get root context and look for Clipboard - QQmlContext *rootCtx = engine->rootContext(); - if (!rootCtx) { - fprintf(stderr, "[grimoire] No root context\n"); - return false; + /* Tablet release at last point */ + const auto &last = line.points.last(); + QPointF lpos(last.x + xOff, last.y + yOff); + + QTabletEvent release( + QEvent::TabletRelease, penDevice, + lpos, lpos, + last.pressure / 255.0, + 0, 0, 0, 0, 0, + Qt::NoModifier, + Qt::LeftButton, + Qt::NoButton + ); + QCoreApplication::sendEvent(inputTarget, &release); } - QObject *clipboard = rootCtx->contextProperty("Clipboard").value(); - if (!clipboard) { - fprintf(stderr, "[grimoire] No Clipboard context property found\n"); - // List available context properties for debugging - fprintf(stderr, "[grimoire] Trying to find clipboard-like objects...\n"); - return false; - } - - fprintf(stderr, "[grimoire] Found Clipboard object at %p\n", (void*)clipboard); - - // Set items on the clipboard model - // The clipboard model has an 'items' property that accepts QList> - QVariant itemsVariant = QVariant::fromValue(m_items); - bool ok = clipboard->setProperty("items", itemsVariant); - fprintf(stderr, "[grimoire] Set Clipboard.items: %s (%d items)\n", - ok ? "ok" : "FAILED", m_items.size()); - - return ok; + fprintf(stderr, "[grimoire] Posted all tablet events\n"); } int GrimoireInjector::loadStrokes(const QString& path) { diff --git a/xovi-ext/grimoire-injector/GrimoireInjector.hpp b/xovi-ext/grimoire-injector/GrimoireInjector.hpp index 80db854..c1a8728 100644 --- a/xovi-ext/grimoire-injector/GrimoireInjector.hpp +++ b/xovi-ext/grimoire-injector/GrimoireInjector.hpp @@ -16,7 +16,6 @@ public: Q_INVOKABLE bool setupVtable(); Q_INVOKABLE int itemCount() const { return m_items.size(); } Q_INVOKABLE bool isReady() const { return m_vtableReady; } - Q_INVOKABLE bool injectToClipboard(); public slots: void loadAndInject(); diff --git a/xovi-ext/grimoire-injector/entry.c b/xovi-ext/grimoire-injector/entry.c index 0fc0ed0..39e2e39 100644 --- a/xovi-ext/grimoire-injector/entry.c +++ b/xovi-ext/grimoire-injector/entry.c @@ -1,20 +1,105 @@ #define _GNU_SOURCE #include #include +#include +#include +#include +#include #include "xovi.h" void registerGrimoire(); extern char *program_invocation_short_name; -static int is_worker = 0; +/* Global state captured by hooks */ +static void *g_qmlEngine = NULL; +static void *g_framebufferAddr = NULL; +static int g_fbWidth = 0, g_fbHeight = 0, g_fbBpl = 0, g_fbFormat = 0; +static void *g_sceneController = NULL; -int isGrimoireWorker() { return is_worker; } +/* Hot-reload support */ +static volatile sig_atomic_t g_reloadRequested = 0; + +static void sigusr1Handler(int sig) { + (void)sig; + g_reloadRequested = 1; +} + +/* Exports for GrimoireInjector.cpp to access */ +void *grimoire_getQmlEngine(void) { return g_qmlEngine; } +void *grimoire_getSceneController(void) { return g_sceneController; } +int grimoire_checkReload(void) { + if (g_reloadRequested) { + g_reloadRequested = 0; + return 1; + } + return 0; +} +void *grimoire_getFramebuffer(int *w, int *h, int *bpl, int *fmt) { + if (w) *w = g_fbWidth; + if (h) *h = g_fbHeight; + if (bpl) *bpl = g_fbBpl; + if (fmt) *fmt = g_fbFormat; + return g_framebufferAddr; +} + +/* Hook: QImage::QImage(uchar*, int w, int h, int bpl, Format, CleanupFn, void*) */ +void override$_ZN6QImageC1EPhiiiNS_6FormatEPFvPvES2_( + void *self, void *data, int w, int h, int bpl, int fmt, void *cleanup, void *info) +{ + bool isRM2 = (w == 1404 && h == 1872 && + ((bpl == 2808 && fmt == 7) || (bpl == 5616 && fmt == 4))); + if (isRM2 && g_framebufferAddr == NULL) { + g_framebufferAddr = data; + g_fbWidth = w; + g_fbHeight = h; + g_fbBpl = bpl; + g_fbFormat = fmt; + fprintf(stderr, "[grimoire] Framebuffer captured: %p %dx%d bpl=%d fmt=%d\n", + data, w, h, bpl, fmt); + } + $_ZN6QImageC1EPhiiiNS_6FormatEPFvPvES2_(self, data, w, h, bpl, fmt, cleanup, info); +} + +/* + * Try to hook SceneController::addDrawingLine at runtime via dlsym. + * The symbol may be stripped, so we check before hooking. + */ +typedef void (*AddDrawingLineFn)(void *closure, void *scene); +static AddDrawingLineFn g_origAddDrawingLine = NULL; + +static void hookedAddDrawingLine(void *closure, void *scene) { + void **captured = (void **)closure; + void *sc_this = captured[0]; + + fprintf(stderr, "[grimoire] addDrawingLine called! closure=%p scene=%p this=%p\n", + closure, scene, sc_this); + for (int i = 0; i < 4; i++) { + fprintf(stderr, "[grimoire] closure[%d] = %p\n", i, captured[i]); + } + + if (g_sceneController == NULL) { + g_sceneController = sc_this; + fprintf(stderr, "[grimoire] Captured SceneController @ %p\n", sc_this); + } + + if (g_origAddDrawingLine) { + g_origAddDrawingLine(closure, scene); + } +} void _xovi_construct() { if (strstr(program_invocation_short_name, "worker") != NULL) { - is_worker = 1; return; } - printf("[grimoire] Main process (%s), registering\n", program_invocation_short_name); + printf("[grimoire] Main process (%s), registering hooks + singleton\n", + program_invocation_short_name); + + /* Install SIGUSR1 handler for hot-reload signaling */ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigusr1Handler; + sigaction(SIGUSR1, &sa, NULL); + fprintf(stderr, "[grimoire] SIGUSR1 handler installed (PID %d)\n", getpid()); + registerGrimoire(); } diff --git a/xovi-ext/grimoire-injector/grimoire-inject.qmd b/xovi-ext/grimoire-injector/grimoire-inject.qmd new file mode 100644 index 0000000..a00e8ea --- /dev/null +++ b/xovi-ext/grimoire-injector/grimoire-inject.qmd @@ -0,0 +1,17 @@ +AFFECT /src/xofm/modules/library/ui/qml/EditDocument.qml + IMPORT dev.grimoire.injector 0.1 + + TRAVERSE ?#sceneController + LOCATE AFTER ALL + INSERT { + Timer { + id: grimoireTimer + interval: 500 + repeat: false + onTriggered: { + GrimoireInjector.loadAndInject() + } + } + } + END TRAVERSE +END AFFECT diff --git a/xovi-ext/grimoire-injector/grimoire-injector.xovi b/xovi-ext/grimoire-injector/grimoire-injector.xovi index e5d5119..0a650d6 100644 --- a/xovi-ext/grimoire-injector/grimoire-injector.xovi +++ b/xovi-ext/grimoire-injector/grimoire-injector.xovi @@ -1 +1,9 @@ version 0.1.0 + +; Hook QImage constructor to capture framebuffer +import? _ZN6QImageC1EPhiiiNS_6FormatEPFvPvES2_ +override _ZN6QImageC1EPhiiiNS_6FormatEPFvPvES2_ +with + $argsize = 8 +end + diff --git a/xovi-ext/uinject/uinject.c b/xovi-ext/uinject/uinject.c new file mode 100644 index 0000000..d1ae119 --- /dev/null +++ b/xovi-ext/uinject/uinject.c @@ -0,0 +1,138 @@ +/* uinject.c — Inject pen strokes via evdev for reMarkable 2 */ +#include +#include +#include +#include +#include +#include +#include +#include + +static void emit_event(int fd, int type, int code, int value) { + struct input_event ev; + memset(&ev, 0, sizeof(ev)); + ev.type = type; + ev.code = code; + ev.value = value; + write(fd, &ev, sizeof(ev)); +} + +static void emit_syn(int fd) { + emit_event(fd, EV_SYN, SYN_REPORT, 0); +} + +/* Convert .rm v6 coordinates to Wacom digitizer coordinates */ +/* RM v6: x[-702,701] y[0,1871], screen portrait 1404x1872 */ +/* Wacom digitizer: x[0,20966] y[0,15725], landscape orientation */ +/* 90° CCW rotation: wacom_x = (screen_height - screen_y) * scale, wacom_y = screen_x * scale */ +static void rm_to_wacom(float rm_x, float rm_y, int *wx, int *wy) { + float screen_x = rm_x + 702.0f; /* 0..1403 */ + float screen_y = rm_y; /* 0..1871 */ + /* Rotate 90° CCW and scale to Wacom range */ + *wx = (int)((1872.0f - screen_y) * 20966.0f / 1872.0f); + *wy = (int)(screen_x * 15725.0f / 1404.0f); +} + +static int rm_pressure_to_wacom(int rm_pressure) { + /* RM pressure: 0-255, Wacom: 0-4095 */ + return rm_pressure * 4095 / 255; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + /* Write directly to the real Wacom digitizer device */ + const char *dev_path = "/dev/input/event1"; + int fd = open(dev_path, O_WRONLY); + if (fd < 0) { + perror("open /dev/input/event1"); + return 1; + } + fprintf(stderr, "[uinject] Writing to %s\n", dev_path); + + /* Read JSON file - simple parser for our known format */ + FILE *fp = fopen(argv[1], "r"); + if (!fp) { + perror("fopen"); + close(fd); + return 1; + } + + /* Read entire file */ + fseek(fp, 0, SEEK_END); + long fsize = ftell(fp); + fseek(fp, 0, SEEK_SET); + char *json = malloc(fsize + 1); + fread(json, 1, fsize, fp); + json[fsize] = '\0'; + fclose(fp); + + fprintf(stderr, "[uinject] Loaded %ld bytes from %s\n", fsize, argv[1]); + + /* Simple JSON parsing - find each stroke's points array */ + /* Format: [{"points":[[x,y,speed,width,direction,pressure],...], ...}, ...] */ + char *p = json; + int stroke_count = 0; + + while ((p = strstr(p, "\"points\"")) != NULL) { + p = strchr(p, '['); /* Find start of points array */ + if (!p) break; + p++; /* Skip [ */ + + /* Pen down */ + emit_event(fd, EV_KEY, BTN_TOOL_PEN, 1); + emit_event(fd, EV_KEY, BTN_TOUCH, 1); + + int point_count = 0; + while (*p && *p != ']') { + /* Parse [x, y, speed, width, direction, pressure] */ + float x, y; + int speed, width, direction, pressure; + if (sscanf(p, "[%f,%f,%d,%d,%d,%d]", &x, &y, &speed, &width, &direction, &pressure) == 6) { + int wx, wy; + rm_to_wacom(x, y, &wx, &wy); + int wp = rm_pressure_to_wacom(pressure); + + emit_event(fd, EV_ABS, ABS_X, wx); + emit_event(fd, EV_ABS, ABS_Y, wy); + emit_event(fd, EV_ABS, ABS_PRESSURE, wp); + emit_event(fd, EV_ABS, ABS_DISTANCE, 0); + emit_syn(fd); + point_count++; + + /* Small delay between points (~5ms) */ + usleep(5000); + } + + /* Advance to next point */ + p = strchr(p, ']'); + if (p) p++; + /* Skip comma and whitespace */ + while (*p && (*p == ',' || *p == ' ' || *p == '\n' || *p == '\r')) p++; + } + + /* Pen up */ + emit_event(fd, EV_ABS, ABS_PRESSURE, 0); + emit_event(fd, EV_ABS, ABS_DISTANCE, 86); + emit_syn(fd); + emit_event(fd, EV_KEY, BTN_TOUCH, 0); + emit_event(fd, EV_KEY, BTN_TOOL_PEN, 0); + emit_syn(fd); + + stroke_count++; + fprintf(stderr, "[uinject] Stroke %d: %d points\n", stroke_count, point_count); + + /* Small delay between strokes */ + usleep(50000); + } + + fprintf(stderr, "[uinject] Done: %d strokes injected\n", stroke_count); + + free(json); + close(fd); + + return 0; +}