Initial commit: Chatter — assistive-writing app for reMarkable Paper Pro Move
Direct-framebuffer ink pipeline (stock-quality strokes), finger-wipe erase, growable scrolling canvas with color-ghost cleanup, bidirectional toggle with a persistent 4-finger return launcher, instant button feedback. Includes prebuilt aarch64 binaries (dist/), build/deploy/install scripts, a user guide, and a complete technical reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
tools/chatter_launcher.c
Normal file
86
tools/chatter_launcher.c
Normal file
@@ -0,0 +1,86 @@
|
||||
// Chatter launcher daemon. Runs always (systemd service), reads the touch device
|
||||
// (event3) WITHOUT grabbing it, and when it sees a multi-finger hold gesture
|
||||
// (>= FINGERS contacts held >= HOLD_MS) while xochitl is the active app, it
|
||||
// switches to Chatter. This is the "return to Chatter from the standard
|
||||
// interface" trigger (xochitl has no plugin API to add a real button).
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DEV "/dev/input/event3"
|
||||
#define MAX_SLOTS 16
|
||||
#define FINGERS 4 // contacts required
|
||||
#define HOLD_MS 700 // how long they must be held
|
||||
#define COOLDOWN_MS 4000 // ignore re-triggers for this long
|
||||
|
||||
static long now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
long lastTrigger = 0;
|
||||
fprintf(stderr, "chatter-launcher: started (fingers=%d hold=%dms)\n", FINGERS, HOLD_MS);
|
||||
|
||||
// Outer loop: (re)open the device forever, so a read interruption (e.g. the
|
||||
// device sleeping/waking) never kills the daemon.
|
||||
for (;;) {
|
||||
int fd = open(DEV, O_RDONLY);
|
||||
if (fd < 0) { sleep(2); continue; }
|
||||
|
||||
int slot = 0;
|
||||
int tid[MAX_SLOTS];
|
||||
for (int i = 0; i < MAX_SLOTS; i++)
|
||||
tid[i] = -1;
|
||||
long holdStart = 0;
|
||||
int triggered = 0;
|
||||
|
||||
struct input_event ev;
|
||||
while (read(fd, &ev, sizeof(ev)) == (ssize_t)sizeof(ev)) {
|
||||
if (ev.type == EV_ABS) {
|
||||
if (ev.code == ABS_MT_SLOT) {
|
||||
slot = ev.value;
|
||||
if (slot < 0) slot = 0;
|
||||
if (slot >= MAX_SLOTS) slot = MAX_SLOTS - 1;
|
||||
} else if (ev.code == ABS_MT_TRACKING_ID) {
|
||||
tid[slot] = ev.value; // >=0 active, -1 released
|
||||
}
|
||||
} else if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
|
||||
int active = 0;
|
||||
for (int i = 0; i < MAX_SLOTS; i++)
|
||||
if (tid[i] >= 0) active++;
|
||||
|
||||
const long t = now_ms();
|
||||
if (active >= FINGERS) {
|
||||
if (holdStart == 0)
|
||||
holdStart = t;
|
||||
else if (!triggered && (t - holdStart) >= HOLD_MS &&
|
||||
(t - lastTrigger) >= COOLDOWN_MS) {
|
||||
triggered = 1;
|
||||
lastTrigger = t;
|
||||
if (system("systemctl is-active --quiet xochitl") == 0) {
|
||||
fprintf(stderr, "chatter-launcher: gesture -> switching to Chatter\n");
|
||||
system("/home/root/chatter/to-chatter.sh >/dev/null 2>&1 &");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holdStart = 0;
|
||||
triggered = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(fd);
|
||||
fprintf(stderr, "chatter-launcher: input read ended, reopening\n");
|
||||
sleep(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user