이 저장소가 대체 무엇인가.
riddle은 reMarkable Paper Pro라는 전자잉크(e-ink) 태블릿을 위한 "마법 일기장" 앱이다. 스타일러스로 페이지에 글을 쓰고 잠깐(약 2.8초) 손을 멈추면, 앱이 그 잉크를 종이 속으로 흩어 없애고, 화면에 쓰인 손글씨를 이미지 한 장으로 만들어 비전(vision) LLM에게 보낸다. 그러면 모델의 답이 흘림체 손글씨로 한 획씩 종이 위에 나타났다가, 잠시 뒤 다시 스르륵 사라진다.
해리 포터에서 톰 리들의 일기장은 글을 쓰면 그 잉크를 빨아들이고, 보이지 않는 손이 흘림체로 답을 써 준다. riddle은 정확히 그 장면을 실제 e-ink 태블릿에서 구현한다.
다만 답을 쓰는 '손'은 마법이 아니라 비전 LLM이고, "잉크를 마시는" 연출은 픽셀을 해시로 하나씩 지우는 용해(dissolve) 애니메이션이다. 페르소나도 실제로 "50년간 이 일기장에 보존된 톰 마볼로 리들의 기억"으로 프롬프트에 박혀 있다.
코드상 라이선스는 관대한 MIT(저작권 2026 Maxime Rivest)이며, 클론 시점 최신 커밋은 2026-07-06, 전체 커밋 11개의 초기 프로젝트다. 언어 구성은 Rust 82% · C 7.7% · C++ 6.1% · Shell 4.2%로, 앱 본체(Rust) 약 2,900줄 + 화면 장악용 C/C++(quill) 약 1,500줄 규모다. TrendShift에는 "New 2026"으로 오늘 Daily 11위에 올라 있다.
"AI가 손글씨로 답한다"는 겉모습이다. 이 레포의 진짜 가치는 raw evdev 입력 파싱 · 폰트 래스터화→골격화(Zhang-Suen)→획 추적 · e-ink 파형/부분갱신 제어 · dlsym 심볼 인터포지션으로 벤더 라이브러리 버퍼 가로채기 · SDK 없이 손으로 쓴 base64/JSON/SSE 스트리밍 LLM 호출 · Rust 상태기계 + mpsc 채널을 3,000줄 남짓한 한 저장소에서 통째로 볼 수 있다는 점이다. 작지만 밀도가 대단히 높다.
분석 중 확인한 바로, GitHub 웹 렌더 페이지는 클론한 소스보다 오래된 README를 보여줬다(별점 0·릴리스 없음으로 표시). 이 문서의 버전·구조·코드는 전부 shallow clone한 실제 소스(HEAD 2026-07-06) 기준이며, 별점 수치는 신뢰할 수 없어 인용하지 않는다. 프로젝트 자체가 초기 단계이고 단일 기종(reMarkable Paper Pro)에서만 검증됐다는 점도 감안하자.
트렌딩 이유와, "그냥 챗봇 쓰기"와의 결정적 차이.
riddle의 전제는 챗봇을 떠올리게 하는 모든 요소를 지우는 것이다 — 텍스트 상자, 전송 버튼, 메시지 버블, 화면 발광까지. 남는 것은 종이 위의 잉크뿐이다. LLM을 "마법 아날로그 물건"처럼 느끼게 만드는 것이 목표이고, 매체(e-ink+손글씨) 자체가 제품이다. 요즘 쏟아지는 "AI 래퍼" 중에서도 방향이 완전히 다르다.
일반 AI 앱 = 키보드로 치고, 버튼 누르고, 말풍선으로 읽는다(도구를 의식한다). riddle = 펜으로 쓰면 종이가 알아서 먹고, 답이 손글씨로 배어 나온다(도구를 잊는다).
모델은 내가 타이핑한 글자를 받지 않는다. 내 손글씨를 찍은 PNG 이미지를 비전 모델이 "읽는다". 답 역시 화면에 텍스트로 뿌리는 게 아니라, Dancing Script 폰트를 1픽셀 골격으로 얇게 깎아 획 순서대로 그려 "보이지 않는 손이 쓰는" 것처럼 애니메이션한다. 입출력 양쪽 모두 사람의 필기라는 점이 이 프로젝트의 정체성이다.
e-ink는 느리고 LLM도 느리다. riddle은 이걸 연출로 가린다. 페이지가 커밋되는 즉시 LLM 요청을 딴 스레드에서 쏘아 놓고, 그동안 "잉크를 마시는" 14단계 용해 + 맥동하는 생각 표시 애니메이션을 돌린다. 응답은 문장 단위로 스트리밍되어, 모델이 답을 다 만들기 전에 이미 첫 문장을 쓰기 시작한다. 체감 지연이 크게 줄어드는 영리한 설계다.
가장 빠른 펜-투-글라스 응답을 얻기 위해, riddle의 "takeover(장악)" 모드는 reMarkable 기본 UI(xochitl)를 멈추고 벤더의 독점 e-ink 엔진 libqsgepaper.so를 직접 구동한다. 이를 위해 dlsym(RTLD_NEXT)로 Qt의 QImage 생성자를 가로채 엔진 내부 프레임버퍼를 낚아채는 기법(epfb.cpp)을 쓴다. "리버스 엔지니어링 없이 이 기기에서 얻을 수 있는 가장 낮은 지연의 서드파티 드로잉 경로"라고 저자가 설명할 정도다.
손글씨 일기장이라는 용도는 부차적이다. 진짜 가치는 커널 레벨 입력 장치 다루기 · 고전 이미지처리(세선화·획 추적) · 임베디드 디스플레이 내부(파형·부분갱신) · ELF 심볼 인터포지션 · SDK 없는 스트리밍 LLM 통합 · 안전한 기기 장악/복원 UX를 실제 동작하는 소형 코드로 한 번에 관찰할 수 있다는 것이다.
무엇으로 만들어졌나 — 계층별로. (Cargo.toml·Cargo.lock·build 스크립트에서 실제 확인)
레포는 두 부분으로 나뉜다. riddle/(Rust)는 앱 본체 — 입력·상태기계·LLM·손글씨 합성·그리기를 담당한다. quill/(C/C++·Qt6)는 "화면 장악" 백엔드 — 벤더 e-ink 엔진을 감싸 libquill.so를 만든다. Rust 앱은 창(windowed) 모드에선 quill 없이도 돌지만, 최저 지연의 takeover 모드에선 quill을 FFI로 부른다.
| 크레이트 | 버전 (선언 → 잠금) | 역할 |
|---|---|---|
| libc | 0.2 → 0.2.186 | raw open/read/ioctl — evdev 입력 장치를 직접 읽기 |
| signal-hook | 0.3 → 0.3.18 | SIGTERM/SIGINT → 깨끗한 종료 플래그 |
| png | 0.17 → 0.17.16 | 손글씨 크롭을 흑백 PNG로 인코딩(오라클 전송용) |
| ab_glyph | 0.2 → 0.2.32 | Dancing Script 글리프를 픽셀 마스크로 래스터화 |
| ureq | 2.10 → 2.12.1 (tls, no-default) | 순수 Rust HTTPS 클라이언트 — OpenAI 호환 오라클 호출 |
| rustls / ring | 0.23.41 / 0.17.14 (전이) | TLS를 OpenSSL·C 없이 — 자기완결 바이너리 + 크로스컴파일 용이 |
oracle.rs에서 문자열 스캔으로 손수 파싱하고, base64도 직접 구현, 비동기 런타임 대신 OS 스레드 + std::sync::mpsc를 쓴다. "무거운 의존성 없이 단일 바이너리로" 라는 규율이 뚜렷하다. 릴리스 프로필도 strip=true, lto=true.| 항목 | 값 | 메모 |
|---|---|---|
| 언어/프레임워크 | C++ · Qt 6 (QImage/QRect) | 벤더 독점 libqsgepaper.so(e-ink 씬그래프 플러그인)에 링크 |
| 빌드 툴체인 | reMarkable "ferrari" SDK 3.26 | aarch64-remarkable-linux-gcc, glibc 2.38 sysroot |
| 산출물 | libquill.so + 데모들 | scribble · map_demo · image_demo · gif_demo · drawlab 등 |
| 항목 | 값 |
|---|---|
| 기기 | reMarkable Paper Pro (코드명 ferrari, aarch64), OS 3.26~3.27, 화면 1620×2160 |
| 디지타이저 | Elan 마커, 좌표 최대 11180×15340, 압력 0~4096 (pen.rs) |
| 실행 환경 | root 권한 + 개발자 모드, xovi + AppLoad 런처 위에서 구동 |
| 설치 | 동반 툴 remagic(remagic install riddle)이 가장 쉬움 · 또는 프리빌드 zip을 scp · 또는 소스 크로스컴파일 |
펜 입력 → LLM → 손글씨 출력까지, 하나의 이벤트 루프로.
앱 전체는 riddle/src/main.rs의 단일 이벤트 루프이고, enum State로 표현된 유한 상태기계(FSM)가 흐름을 지휘한다. 무거운 작업(LLM)은 별도 스레드에서 돌고, 결과는 std::sync::mpsc 채널로 루프에 되돌아온다.
oracle.ask(png, tx)는 답 조각들을 tx로 계속 흘려보내다가, 다 보내면 sender를 그냥 떨군다(drop). 수신 쪽은 Disconnected를 보고 "완료"를 안다. 별도 "끝" 메시지 타입 없이, 언어의 채널 의미론만으로 스트리밍 종료를 표현하는 의존성 없는 우아한 프로토콜이다.펜은 벤더 UI를 거치지 않고 커널 입력 장치를 날것으로 읽는다. 64비트에서 각 input_event는 24바이트(타임스탬프 16 + type 2 + code 2 + value 4)이고, SYN_REPORT 프레임 사이의 값을 모아 한 샘플로 방출한다:
for chunk in buf[..n].chunks_exact(24) {
let etype = u16::from_le_bytes(chunk[16..18].try_into().unwrap());
let code = u16::from_le_bytes(chunk[18..20].try_into().unwrap());
let value = i32::from_le_bytes(chunk[20..24].try_into().unwrap());
match (etype, code) {
(EV_ABS, ABS_PRESSURE) => { self.pressure = value; }
(EV_KEY, BTN_TOOL_RUBBER) => { self.tool = if value==1 {Eraser} else {Pen}; }
(EV_SYN, SYN_REPORT) => { /* 원좌표 → 화면좌표 스케일 후 방출 */
x: raw_x * (SCREEN_W - 1) / DIGI_MAX_X, ... }
_ => {}
}
}
장치는 ioctl(fd, EVIOCGRAB, 1)로 독점(grab)해, 기본 UI가 같은 펜에 이중 반응하지 않게 막는다. 붓 반지름은 압력에 비례한다: let r = 2 + pressure * 3 / MAX_PRESSURE;
잉크가 사라지는 연출은 난수가 아니라 좌표 기반 해시로 만든다. 각 단계마다 "해시값이 현재 단계 이하인" 잉크 픽셀만 지우므로, 14패스에 걸쳐 안정적인 얼룩 패턴으로 증발한다:
fn px_hash(x: i32, y: i32) -> u32 {
let mut h = (x as u32).wrapping_mul(0x9E3779B1)
^ (y as u32).wrapping_mul(0x85EBCA6B);
h ^= h >> 13; h = h.wrapping_mul(0xC2B2AE35); h ^ (h >> 16)
}
// dissolve_pass: stage/stages 진행도에 따라 픽셀을 흰색으로
if surf.luma(x, y) < 250 && px_hash(x, y) % stages <= stage {
surf.put_px(x, y, WHITE); // 이 픽셀 지금 증발
}
이미지 픽셀 수가 비전 토큰 비용·지연을 좌우하므로, 잉크 bbox로 자른 뒤 긴 변이 800px 이하가 되도록 박스 다운스케일한다. 인코딩은 파일 크기보다 속도 우선(Compression::Fast, 그레이스케일):
let f = ((x1 - x0).max(y1 - y0)).div_ceil(800).max(2); // 축소 배율
// f×f 블록의 휘도를 평균 → 8비트 그레이스케일 버퍼
enc.set_color(png::ColorType::Grayscale);
enc.set_compression(png::Compression::Fast);
기본 백엔드 HttpOracle는 손글씨 PNG를 base64 data-URI로 실어 /chat/completions에 스트리밍(SSE) 요청을 보낸다. 본문은 serde 없이 포맷 문자열로 직접 조립한다:
let body = format!(concat!(
"{{\"model\":{},\"stream\":true,\"max_tokens\":{},",
"\"messages\":[{{\"role\":\"system\",\"content\":{}}},",
"{{\"role\":\"user\",\"content\":[",
"{{\"type\":\"text\",\"text\":{}}},",
"{{\"type\":\"image_url\",\"image_url\":{{\"url\":",
"\"data:image/png;base64,{}\"}}}}]}}]}}"),
json_quote(&model), max_tokens,
json_quote(PERSONA), json_quote("Reply to what is written in the diary."), img);
let resp = ureq::post(&format!("{base}/chat/completions"))
.set("Authorization", &format!("Bearer {key}"))
.send_string(&body); // SSE 라인(data: {...})을 한 줄씩 파싱
SSE 델타에서 choices[0].delta.content를 뽑고, sentence_cut()으로 한 문장이 완성될 때마다 채널로 넘겨 quill이 일찍 쓰기 시작하게 한다. 페르소나는 두 백엔드가 공유한다:
const PERSONA: &str = "You are the memory of Tom Marvolo Riddle,
preserved in this enchanted diary for fifty years. ...
Keep replies SHORT: one to three sentences, like ink appearing on a page.
Never mention images, photos, models or AI; you only ever perceive words
written in the diary. ... Always answer in the language the writer used.";
대안 백엔드 PiOracle는 Node 프로세스(pi --mode rpc)를 계속 띄워둔 채 stdin/stdout으로 JSONL 이벤트를 주고받아, 매 턴 모델 지연만 지불한다(구독 인증 재사용).
script.rs는 답변 텍스트를 그릴 수 있는 펜 경로로 바꾼다. 3단계다:
1) rasterize_line : ab_glyph로 Dancing Script 글리프를 채운 마스크(cov > 0.5)
2) thin : Zhang-Suen 세선화 → 글자를 1픽셀 두께 골격으로
// 이웃 흑점 수 b가 2..=6, 0→1 전이 a==1, 두 조건 c1·c2를 만족하면 제거
if !(2..=6).contains(&b) { continue; }
if a != 1 { continue; }
if c1 && c2 { to_clear.push(idx(x, y)); } // 두 위상 반복 → 골격만 남김
3) trace : 골격을 순서 있는 폴리라인(획)으로 추적, 왼→오 정렬
그런 다음 main.rs::plan_reply()가 획을 화면 좌표로 매핑하며 줄마다 미세한 jitter()/흔들림을 더해 "사람이 쓴 듯"하게 만들고, State::Replying이 틱마다 약 26점씩 brush_line으로 그린다. 스트리밍으로 나중에 온 문장은 append_reply()가 그리는 도중에 아래로 이어 붙인다.
e-ink 엔진 libqsgepaper.so는 프레임버퍼를 내부 QImage로만 갖고 있어 공개 접근자가 없다. quill/src/epfb.cpp는 dlsym(RTLD_NEXT)로 Qt의 QImage 생성자(맹글된 심볼)를 가로채, 초기화 구간에 엔진이 만드는 QImage들을 픽셀 포맷 인자로 구분해 기록한다:
extern "C" void _ZN6QImageC1EPhiixNS_6FormatE...(QImage *that, ...) {
ORG(...); // 진짜 생성자를 먼저 호출
org(that, ...);
if (state != 1) return;
if (param_5 == 4 || param_5 == 7) pointerTracker[that] = SWTCON_TypeA; // 보조 FB
else if (param_5 == 0x18) pointerTracker[that] = SWTCON_TypeB; // 메인 FB
}
quill_c.cpp가 이 보조 QImage의 raw bits()를 quill_buffer()로 노출하고, riddle이 여기에 RGB32를 직접 써넣은 뒤 EPFramebuffer::swapBuffers(rect, Mono, mode, flags)로 화면에 밀어낸다. 파형 모드: 0=최고속(잉크), 3=중간(텍스트/애니), 4=전체 플래시 클리어.
surface.rs의 Surface는 qtfb의 RGB565(2바이트/픽셀)와 quill의 RGB32를 put_px에서 변환해 흡수한다. 덕분에 모든 드로잉 코드는 포맷을 몰라도 된다. 갱신은 fb.rs의 grow-only BBox(더티 렉트)로 바뀐 영역만, 그것도 8ms(takeover)/35ms(창) 간격으로 병합해 밀어낸다 — e-ink 부분 갱신이 비싸기 때문.
약 3,000줄이 어디에 어떻게 놓였나. (괄호 안 숫자는 대략 줄 수)
구조가 곧 설명서다. 한 파일 = 한 관심사로 깔끔히 쪼개져 있어(입력=pen/touch/power, 두뇌=main/oracle, 표현=script/surface/ink, 출력=display/qtfb), 3,000줄임에도 어디를 봐야 할지 즉시 안다. ★ 표시한 script.rs(손글씨 합성)와 epfb.cpp(버퍼 가로채기)가 기술적으로 가장 배울 것이 많은 두 파일이다.
이 작은 코드베이스에서 훔쳐 배울 수 있는 것들.
evdev의 input_event 구조체를 손으로 파싱하고, EVIOCGRAB로 장치를 독점하며, /sys/class/input/*/device/name으로 펜·지우개·터치·전원 장치를 구분한다(pen.rs·touch.rs·power.rs). GUI 프레임워크를 건너뛰고 커널과 직접 대화하는 법의 표본.
script.rs의 Zhang-Suen 세선화(두 위상 이웃 연결성 판정을 고정점까지 반복)와 골격 → 순서 있는 획 추적은, 그대로 떼어내 어떤 캔버스에도 쓸 수 있는 깔끔한 CS 연습이다. 폰트 커버리지 마스크 → 1픽셀 골격 → 애니메이션 가능한 획으로 이어지는 흐름을 통째로 본다.
OpenAI /chat/completions 비전 요청을 손으로 만들고, SSE 델타를 파싱하고, 문장 단위로 잘라 체감 지연을 줄이며, 두 번째 백엔드는 상주 서브프로세스와 stdio JSONL로 통신한다(oracle.rs). base64/JSON까지 직접 구현해 의존성을 극도로 얇게 유지하는 취향도 배운다.
enum State 전이, 크로스 스레드 스트리밍을 위한 mpsc, 그리고 "sender를 떨궈 완료를 알리는" 관용을 main.rs에서 실물로 본다. 비동기 런타임 없이 OS 스레드만으로 반응형 UI 루프를 구성하는 미니멀리즘.
파형 모드, 전체 vs 부분 갱신, 더티 렉트 병합, 그리고 하이라이트인 심볼 인터포지션(dlsym(RTLD_NEXT)) 심으로 벤더 라이브러리의 내부 버퍼를 가로채기(epfb.cpp). 고급이지만 주석이 잘 달려 있어 ELF 링킹·심볼 해석을 공부하기에 좋다.
기본 UI(xochitl)를 멈추고 화면을 뺏는 앱은 잘못되면 태블릿을 먹통으로 만든다. riddle은 transient systemd 유닛으로 분리 실행(런처가 죽어도 살아남게)하고, trap restore EXIT INT TERM으로 무슨 일이 있어도 xochitl을 되살리며, 절전은 커널 성공 카운터로 확인한다. "사용자 기기를 망가뜨리지 않기"라는 안전망 설계를 배운다.
돌리려면 / 빌드하려면 무엇이 필요한가.
| 구분 | 요구사항 | 메모 |
|---|---|---|
| 기기 | reMarkable Paper Pro ("ferrari", aarch64) | OS 3.26~3.27, 개발자 모드 + root. 이 기종에서만 검증됨 |
| 런처 | xovi + AppLoad | remagic로 설치가 가장 쉬움(개발자 모드·xovi·AppLoad 자동 세팅) |
| takeover 빌드 | libqsgepaper.so + ferrari SDK | 독점 라이브러리는 본인 기기에서 추출(재배포 안 됨), SDK는 glibc 2.38·Qt6 sysroot |
| 오라클 (택1) | HTTP 백엔드 권장 | RIDDLE_OPENAI_KEY 필수(비전 지원 엔드포인트). _BASE(기본 OpenAI)·_MODEL(기본 gpt-4o-mini)·_MAX_TOKENS(2000) 선택. OpenRouter/Gemini 프리셋 제공 |
| 오라클 (대안) | pi 백엔드 | Node pi가 /home/root/node/bin/pi에 있어야. 키 없을 때 자동 사용 |
| 호스트(빌드 PC) | x86_64 리눅스 + aarch64 크로스툴체인 | 창 모드는 cargo build --release --target aarch64-unknown-linux-gnu |
riddle --oracle-test 손글씨.png기기·디스플레이 없이도 API 키·엔드포인트·모델이 제대로 붙는지 확인할 수 있다. PNG 한 장을 넣으면 스트리밍 답을 콘솔에 뿌린다. 알고리즘(세선화·오라클)은 하드웨어 독립이라 데스크톱에서 개발·검증이 가능하고, quill/takeover만 실제 태블릿+SDK+독점 라이브러리를 요구한다.
README는 다른 기종/OS에서 브릭(먹통)될 수 있으니 SSH 탈출로(root@10.11.99.1)를 항상 열어두라고 경고한다. root로 돌고, 벤더 UI를 멈추고, 독점 라이브러리를 가로채는 만큼 실험적이다. 또한 커밋 이력이 11개뿐인 초기 프로젝트이므로, 재미로 배우기엔 훌륭하지만 "안정 제품"으로 기대하지는 말자.
읽기만 하면 안 남는다 — 대부분 태블릿 없이 데스크톱에서 가능하다.
손글씨가 담긴 PNG 한 장을 준비하고, 비전 지원 API 키를 oracle.env에 넣은 뒤 riddle --oracle-test 손글씨.png를 돌린다(기기 불필요). 목표: "이미지 → 비전 LLM → 스트리밍 텍스트"라는 핵심 파이프의 절반을 손으로 확인.
파이썬으로 검은 글자 이미지를 하나 만들고, script.rs의 세선화 로직(이웃 흑점 수 2~6, 0→1 전이 1회 등)을 그대로 옮겨 1픽셀 골격을 뽑아 본다. 목표: "채워진 글자 → 뼈대"가 어떻게 계산되는지 알고리즘 감각 잡기.
과제 ②의 골격을 입력으로, 끝점(차수 1)부터 시작해 폴리라인으로 잇는 trace를 구현하고, HTML canvas에 한 점씩 그려 "쓰는 것처럼" 재생해 본다. 목표: riddle의 State::Replying이 하는 일을 브라우저에서 재현.
OpenAI 호환 /chat/completions에 stream:true로 요청하고, 응답의 data: {json} 라인에서 delta.content만 뽑아 문장이 끝날 때마다 출력한다(라이브러리 최소화). 목표: oracle.rs의 sentence_cut 아이디어로 "체감 지연 줄이기"를 체득.
ask(png, tx)라는 동일 인터페이스 뒤에 세 번째 백엔드(예: 로컬 llama.cpp 서버)를 붙여 본다. HTTP/pi 두 구현을 참고해 스트리밍 청크 → 채널 → sender drop 계약만 지키면 나머지는 그대로 돈다. 목표: "인터페이스 하나, 구현 여럿" 확장 설계를 손으로 확인.
이 레포를 온전히 읽으려면 — 5주 코스.
| 주차 | 주제 | 무엇을 / 왜 |
|---|---|---|
| 1주차 | Rust 기초 + 스레드 | enum·match·소유권·std::thread·mpsc. riddle의 상태기계와 채널을 읽는 토대 |
| 2주차 | 리눅스 입력/장치 | evdev·ioctl·/dev/input·input_event 구조체. 펜/터치/전원 입력의 원리 |
| 3주차 | 이미지처리·폰트 | 래스터화, 이진화, Zhang-Suen 세선화, 골격 추적. 손글씨 합성의 핵심 |
| 4주차 | HTTP/SSE·LLM API | 비전 메시지 포맷, base64 data-URI, SSE 스트리밍 파싱. 데이터 수집·응답의 심장 |
| 5주차 | ELF 링킹·임베디드 디스플레이 | 동적 링킹·dlsym(RTLD_NEXT) 인터포지션, e-ink 파형·부분 갱신. 가장 난도 높은 quill |
어려운 5주차(quill/심볼 인터포지션)부터 파지 말자. 3주차(세선화)와 4주차(SSE)는 태블릿 없이 데스크톱에서 완주할 수 있어 성취감이 빠르다. 이 둘을 손에 익힌 뒤, 실제 기기가 있다면 그때 5주차의 화면 장악으로 넘어가는 게 좌절이 적다.
문서에 나온 용어를 한 번에.
EVIOCGRAB는 그 장치를 "나만 쓰겠다"고 독점하는 ioctl로, 기본 UI의 이중 반응을 막는다.data: ... 줄을 조금씩 흘려보내는 스트리밍 방식. 답 전체를 기다리지 않고 도착하는 대로 처리(=문장 단위로 쓰기 시작)할 수 있다.std::sync::mpsc는 여러 생산자 → 한 소비자 큐. riddle은 답 조각을 흘리다 보내는 쪽을 떨궈(drop) "끝"을 알린다 — 별도 종료 메시지가 필요 없다.dlsym(RTLD_NEXT)RTLD_NEXT로 진짜 원본도 찾아 부를 수 있어, 엔진 내부 버퍼를 몰래 기록한다.unsafe extern "C"로 libquill.so(C/C++)의 함수를 호출한다.xochitl=reMarkable 기본 UI, xovi+AppLoad=서드파티 앱 런처 생태계, remagic=이 저자가 만든 설치 도우미(개발자 모드·런처 자동 세팅).더 깊이 파고들 곳.