This repo forks the FastRead RSVP reader for one piece of hardware: the Light Phone III's 3.92", 1080×1240 black-and-white OLED (see Credits for the exact upstream lineage — it's more tangled than a single fork arrow). Upstream's reading engine, both ebook parsers and the whole gesture model are untouched here. Everything this fork adds is presentation: a true-black theme, a hardware wheel, and a package identity that installs alongside any upstream FastRead build without colliding.
Current version: baseVersionName in app/build.gradle.kts is 1.2.0-light.1,
bumped for the full-page ereader mode below. CI stamps versionCode and appends
-bN to baseVersionName on every push to main — see Building below.
- Tap the reader's three-line context preview (instead of holding it) to open a full-page ereader: swipe or turn the wheel to move between pages, with an iOS 6-style 3D flip, landing back in RSVP mode wherever you stopped.
- Every upstream screen — library, reader, Settings, the in-reader quick-settings sheet — runs unmodified, repainted for true black.
- The hardware wheel (
hw/LightKeys.kt+hw/Wheel.kt) scrolls the library, Settings, the chapter list, the bookmark list and the quick-settings sheet, including insideDialogsheets, which don't receive key events for free (WheelInDialog()borrows the sheet window's own callback). - Package identity (
com.lightfastread, bothnamespaceandapplicationId) is fully separated from upstream FastRead, so this build and an upstream build coexist on one device with no R-class or provider-authority collisions. - CI is green on
main: unit tests, a signed release build, a certificate-fingerprint check, and a launcher-icon check all gate the release before it publishes.
- 1080×1240 @ ~420dpi ≈ 411×472dp — normal width, about half the usual Android phone height. Anything sized as a fraction of screen height (bottom sheets, FAB clearance in the library list) needed a second look against upstream's assumptions.
- Forced greyscale. LightOS's black-and-white look is Android's accessibility
daltonizer pinned to monochromacy, not a hardware limit, but this fork doesn't fight
it — the theme is greyscale by construction. Title and ORP accents are remapped
through Rec. 709 luma into
0.62..1.0so low-luma picks (the default red ORP letter, luma ≈ 0.36) stay legible next to dimmed context lines instead of sinking into them. - Matte panel, roughly a stop less perceived contrast than glossy glass — alphas tuned for upstream's dark theme (0.45, 0.5) needed their floors lifted here: context lines, the paragraph pilcrow, the progress readout, the zone guides.
- True black kills Material 3 tonal elevation. Once every
surface*role andsurfaceTintare pinned to#000000, nothing distinguishes an elevated row from the background. Hairlineoutlineedges (1dp) stand in for the tonal lift on book rows, the top bar, the chapter strip and the bottom sheet.surfaceVariantand dialog containers stay faintly grey on purpose, since Material's progress/slider/ switch tracks and ephemeral containers (AlertDialog, menus) need something to read against — a scrim over black tints nothing.
git log from c7d1a4f onward is this fork; the repo's Initial commit is the
upstream import point.
2f38b86— Add a full-page ereader mode, opened by tapping the context preview440482b— Say in the README what the wheel needs09fc696— Scroll with the wheelff0bf61— docs: add LightNoise and LightPods to the collection table8d47df9— ci: trigger a second signed build to prove certificate stability4854065— ci: fix the certificate digest parser12e4acc— Sign every release with one stable key so installs can be updatedb3f4c3f— docs: add LightTip to the collection tablebd2f354— Make the reader's zone guides a toggle, off by defaultf135c55— Add a LightFog-matched launcher icon9e09c25— Swap descriptions in README.md tablef1f9009— docs: real screenshots, correct upstream credit, collection table4c48083— Move package to com.lightfastread so it can never collide with FastRead19ff5b4— Rename to LightFastread0947241— docs: document the push-triggered versioned release flow885a95d— ci: auto-publish a versioned APK on every push to main5029b18— build: mark gradlew executable58d1025— ci: fix startup failure and add Android SDKc7d1a4f— Retune for Light Phone III: true-black OLED theme, greyscale palette, near-square layout
Two upstream landmines worth knowing before you build this yourself: gradlew was
committed as mode 100644 (CI dies with exit 126 until it's executable), and
gradle/gradle-daemon-jvm.properties pins the Gradle daemon to JetBrains JDK 21
— setup-java needs distribution: jetbrains to match, or Gradle tries to
provision a second JDK over the network on every run.
A fork of FastRead retuned for the Light Phone III's black-and-white OLED.
Everything upstream does, on a screen where every lit pixel is a choice: pure #000000 on
every surface, a greyscale palette, and layout that assumes a 3.92" near-square panel
instead of a tall glossy one.
Upstream FastRead is unchanged in behaviour here. This fork only touches presentation.
| Change | Why |
|---|---|
| New Light Phone theme mode, default on first run | Kept separate from Dark rather than replacing it, so this fork stays rebaseable on upstream. |
Every surface* role is #000000, surfaceTint is black |
Material 3's dark scheme uses #1C1B1F-ish greys plus tonal elevation overlays. On an OLED that's a lit grey slab where there should be nothing. Black surfaceTint makes surfaceColorAtElevation resolve to black at every elevation. |
| Hairline outlines replace tonal elevation | Once tonal lift is gone, book rows, the top bar, the chapter strip and the bottom sheet are black-on-black with no visible boundary. They get 1dp outline edges instead — one row of dim grey rather than a whole grey rectangle. |
| Title and ORP accents flattened to grey | The panel is greyscale, so the orange title and red ORP letter arrive as whatever grey their luminance happens to be. Rec. 709 luma is remapped into 0.62..1.0 so low-luma picks (the default red, luma ≈ 0.36) stay legible instead of sinking into the dimmed context lines. |
| Contrast floors on de-emphasised text | Matte glass diffuses light and costs roughly a stop of perceived contrast. Context lines, the paragraph pilcrow, the progress readout and the zone guides all had alphas tuned for glossy glass. Relative ordering is preserved; the bottom of the range is lifted. |
| Zone guides are off by default and toggleable | Upstream draws the gesture boundaries permanently. On a pure-black screen that grid competes with the text instead of receding behind it, and it's only genuinely useful while you're still learning where the zones are. |
surfaceVariant and the dialog containers stay faintly grey |
These back Material's tracks (progress bar, slider rails, switch tracks) and its ephemeral containers (AlertDialog, menus). Pure black would erase the tracks entirely, and a scrim over an already-black background tints nothing — a black dialog on a black screen is unanchored text. |
Black windowBackground in themes.xml |
The launch theme is what Android paints during cold start, before Compose draws. The stock Material.Light parent flashed white — a full-brightness strobe on every launch. |
| Book list padded past the FAB; bottom sheets allowed to go taller | The LPIII is roughly 411 × 472 dp — normal width, about half the usual height. Anything sized as a fraction of screen height needed a second look. Scaffold reserves no space for the FAB, so the last book row sat permanently under "Add book". |
| The brightness wheel scrolls every list | The LPIII has a hardware wheel and upstream has never heard of it. Lists here are the one place the touchscreen is worst — a 472dp-tall panel behind matte glass, with the whole reader surface already spoken for by hold-and-swipe gestures. |
targetSdk 34; package fully renamed to com.lightfastread |
LightOS is Android 14, and the light-sdk emulator profile is API 34; no reason to opt into 35/36 behaviour the device will never see. Both namespace and applicationId are com.lightfastread, so this shares no identifier with upstream FastRead — not the package, the R class, or the permission and provider authorities AndroidX derives from them. It installs cleanly alongside any other FastRead build. |
Part of the gi-os Light App collection.
Turning the phone's wheel scrolls the library, the Settings screen, the chapter list, the
bookmark list and the quick-settings sheet. It is not a rotary encoder: LightOS relabels an
optical sensor's scancodes as WHEEL_CCW / WHEEL_CW and dispatches them as ordinary key
events, so hw/LightKeys.kt resolves those labels at runtime and falls back to the raw
scancodes if Light moves them.
Notches arrive faster than a frame, so applying each one where it lands gives a stack of
jumps rather than a scroll. hw/Wheel.kt treats a notch as a debt and pays off a share of it
per frame, and it ignores the first notch after a pause — the wheel sits under a thumb and
catches stray brushes.
Two things this fork needed that the shared module didn't have. The sheets are Dialogs,
and a dialog is a window of its own: the Activity's dispatchKeyEvent never runs while one
is up, so WheelInDialog() borrows the sheet window's callback for as long as the sheet
lives. And the wheel click and camera button are deliberately untouched.
Nothing else has to be installed for any of this, and nothing has to be granted. Light
patched Generic.kl phone-wide, so the notches are ordinary key events delivered to whichever
app holds focus, and this fork reads them itself — no accessibility service, no permission, no
root. Sideload the APK and the wheel scrolls.
LightControl is optional, and it owns the parts of the wheel this app leaves alone: hold the wheel in and turn for brightness, tap it for the flashlight, the camera button to open the camera. Each is rebindable, tap and hold separately, to any installed app, and apps with no wheel code of their own get brightness or a synthetic-swipe scroll instead of a dead wheel.
Installing it doesn't cost you scrolling here. com.lightfastread is on its pass-through list
along with com.gios.* and com.lightrss.reader, so bare turns still arrive as keys and this
app still scrolls per notch — which is the point of the list: a real scroller beats a synthetic
finger.
# Optional: LightControl, for brightness, the flashlight and the camera button
adb install -r LightControl-v1.0.x.apk
# The key service. NOTE: this setting is a list, and this command REPLACES it —
# if you also run LightVoice's push-to-talk, colon-join both components instead.
adb shell settings put secure enabled_accessibility_services \
com.gios.lightcontrol/com.gios.lightcontrol.keys.ControlService
adb shell settings put secure accessibility_enabled 1
# Brightness, and the level readout + opening apps from the service
adb shell appops set com.gios.lightcontrol WRITE_SETTINGS allow
adb shell appops set com.gios.lightcontrol SYSTEM_ALERT_WINDOW allowLatest build: https://github.com/gi-os/LightControl/releases/latest.
An open book: a page of dashed text on the left, collapsed to a single ringed word on the right. That asymmetry is RSVP — the whole book, reduced to one focal point.
Drawn to match LightFog so the two read as siblings on the device: heavy white line art on a full-bleed pure-black square, round caps, 30/1024 outline and 24/1024 detail strokes, ink filling ~62% of the canvas. The ringed dot echoes the pins LightFog's dashed route runs between.
scripts/generate_icon.py defines the geometry once in a 1024-unit space and emits both
the mipmap WebPs and the adaptive icon's VectorDrawable, so the raster and vector can't
drift apart. Re-run it after any change.
Light's own light-sdk is the sanctioned path for LightOS "Tools", and community tools will eventually be built and signed by Light. This is a plain sideloaded APK, not an SDK tool, so for now:
# Every push to main publishes a signed build to Releases:
adb install -r LightFastread-<version>.apkEach release carries exactly one APK, signed with a stable key. Nothing needs uninstalling first; the package ID is unique to this fork.
Add https://github.com/gi-os/LightFastread as a GitHub source. There is one .apk per
release, so there's nothing to disambiguate, and every build is signed with the same key —
updates apply in place.
Releases up to and including build-11 cannot be updated from. The release APK was
unsigned, and the debug APK was signed with the throwaway ~/.android/debug.keystore
that a CI runner regenerates on every job — so consecutive builds had different
certificates. Android rejects both cases, and Obtainium reports either as
Failure: Invalid. If you installed build-11 or earlier, uninstall once, then install
build-12 or later; updates are stable from there on.
Two caveats worth knowing before you start, both from Light's own docs as of July 2026:
- There is no easy distribution path yet. ADB sideloading works if you're comfortable with it, but LightOS builds in the wild aren't ready to play nicely with SDK-built tools.
- LightOS will let you run unsigned/third-party APKs only under the "Any tools" setting, and Light explicitly warns that you own the install/uninstall lifecycle there.
Light publishes an emulator profile that behaves close to real hardware:
- 1080 × 1240, 3.92" display
- Android API 34
- No Google Play Services
That's the configuration this fork was designed against. Note the emulator renders in colour — the real panel is black and white, which is exactly why the palette here is greyscale by construction rather than "colours that happen to look fine in dark mode".
.github/workflows/build.yml builds, tests and publishes on every push to main — no
tagging step needed. Each run:
- builds and tests, then signs the release APK,
- stamps
versionCodewith the workflow run number andversionNamewith a-bNsuffix, so each build is strictly newer than the last, - verifies the signing certificate against
signing-fingerprint.txt, - publishes a GitHub Release tagged
build-N, marked latest, with the single signed APK attached. The debug build stays a workflow artifact, so Obtainium can't pick it up by mistake.
Because versionCode always increases, adb install -r upgrades in place — no uninstall,
no lost library or reading positions.
baseVersionName in app/build.gradle.kts is the single source of truth for the version
number; the workflow greps it to name the artifacts. Bump it there when the version changes.
Tagging v* additionally cuts a release with generated notes, for when you want a
hand-marked version rather than a rolling build.
Signing is mandatory. Four repository secrets drive it — RELEASE_KEYSTORE_BASE64,
RELEASE_STORE_PASSWORD, RELEASE_KEY_ALIAS, RELEASE_KEY_PASSWORD — and the build
fails if the keystore secret is missing rather than quietly emitting an unsigned APK.
The signing certificate's SHA-256 is pinned in signing-fingerprint.txt and CI compares
every build against it. If the keystore is ever lost or replaced, the build fails loudly
instead of shipping an APK that no existing install can update to.
Locally:
./gradlew assembleDebug # requires JDK 17+ (AGP 9.x) and Android SDK 36Drop a keystore.properties next to settings.gradle.kts (storeFile, storePassword,
keyAlias, keyPassword) and local builds — debug included — are signed with the same key
as CI, so they can replace a released build in place. It's gitignored.
Local builds get versionCode = 1, so if a CI build is already on the device you'll need
adb install -r -d to allow the downgrade.
These two are this fork, on a Light Phone III.
The next two come from upstream and still show the original purple and light interface. They are here because this fork changes no behavior on either screen.
| Settings (upstream) | Quick settings (upstream) |
|---|---|
![]() |
![]() |
| Input mode, speed, pauses, fonts, bionic, theme. | In-reader sheet for live font and size changes. |
Most RSVP apps either bury you in buttons, lock features behind a subscription, or require you to push your library into someone else's cloud. FastRead is the opposite:
- One-gesture reading. No play/pause buttons. Hold to read, drag to change speed, release to stop. That's the whole interface.
- Your library stays yours. Books are imported from local storage via the system file picker and cached privately inside the app's own files directory. Nothing leaves the device.
- Native parsers, no bloat. EPUB and MOBI parsing are written from scratch in Kotlin. No third-party ebook libraries pulled in.
- 100% Kotlin + Jetpack Compose. Modern Android stack, single-Activity, navigation-driven, easy to fork and hack on.
- No internet permission requested. The app cannot phone home because it has nothing to phone home with.
- RSVP one-word-at-a-time presentation with a large, configurable focal word.
- Three input modes, switchable in settings:
- Hold zones — hold the right 2/3 of the screen to advance, hold the left 1/3 to step back or auto-rewind.
- Swipe — swipe right to advance, left to go back; distance per word is configurable.
- Zone swipe — combine zones with swipe direction for fine control.
- Optional zone guides. Hairlines along every gesture boundary, toggleable from Settings or the in-reader quick-settings sheet. Off by default — upstream drew them permanently, which on a black screen reads as a bento-box grid over your book.
- Speed mapped to finger position. In hold-zones mode, where you press in the forward zone is your WPM (left edge =
minWpm, right edge =maxWpm). - Smooth ramp-up from 0 to target WPM on press, so speed changes never feel jarring.
- Tap-to-step-back vs. hold-to-rewind in the backward zone, separated by a configurable threshold.
- Three-line context window above the focal word so you don't lose your place when you blink.
- Smart pauses at sentence ends and paragraph breaks, scaled to your current reading speed.
- Per-letter slowdown for long words — give your eyes a few extra milliseconds on the hard ones.
- Bionic reading mode that bolds the leading letters of each word; apply it to the focal word, the context lines, both, or neither.
- Per-book progress saved automatically and resumed on the next open.
- Full-page ereader mode. Tap the context preview instead of holding it to open a whole page of the book; swipe or turn the wheel to move between pages, with an iOS 6-style 3D flip. Closing it resumes RSVP from wherever you landed.
- Import EPUB and MOBI via the Android Storage Access Framework — works with anything in your local storage, on an SD card, or in a cloud-sync folder you've already mounted.
- Long-press to delete with a confirmation dialog.
- At-a-glance progress bars on every book in the library list.
- Material 3 design, with a hand-built greyscale scheme for the Light Phone III.
- System / Light / Dark / Light Phone theme switching. Light Phone is pure
#000000greyscale. - Live font preview in settings: size and family (default, serif, sans-serif, monospace).
- Quick-settings sheet on the reader screen for tweaking font size and family without leaving your book.
| Format | Notes |
|---|---|
.epub (EPUB 2 / 3) |
ZIP container, OPF spine traversal, HTML stripped to plain text. |
.mobi (PalmDOC compression, type 1 & 2) |
Native PDB/PalmDOC decoder. |
.mobi (HUFF/CDIC compression, type 17480) |
Not supported — surfaces a friendly error. |
Rich rendering (images, layout, footnotes) is intentionally out of scope. Speed reading is plain words.
No Play Store listing yet. FastRead is distributed as source and via GitHub Releases (when tagged).
Requirements:
- Android Studio Hedgehog or newer (or just the Android SDK + Gradle)
- JDK 17
- Android SDK 36 with build-tools
Steps:
git clone https://github.com/gi-os/LightFastread.git
cd LightFastread
./gradlew assembleDebug
# APK lands in app/build/outputs/apk/debug/Or open the project in Android Studio and hit Run.
Minimum SDK: 24 (Android 7.0 Nougat). Target SDK: 36.
- Tap Add book on the home screen and pick an
.epubor.mobifile. - Tap the imported book to open the reader.
- Hold the right side of the screen to start reading. Slide your finger left to slow down, right to speed up. Release to stop.
- Tap the left side to step back one word. Hold the left side to auto-rewind.
- Tap the very top of the screen to reveal the title bar with a back button and quick-settings shortcut.
All settings persist via SharedPreferences (JSON-encoded, no database). They can be tweaked live from the Settings screen:
| Setting | Default | What it does |
|---|---|---|
| Min WPM | 100 | Reading speed at the left edge of the forward zone. |
| Max WPM | 500 | Reading speed at the right edge of the forward zone. |
| Speed ramp-up | 300 ms | Time to ease from 0 to target WPM after pressing. |
| Backward hold delay | 500 ms | Tap shorter than this = step back; longer = auto-rewind. |
| Pause after sentence | x1.0 | Multiplier applied to the inter-word interval after ., !, ?. |
| Pause after paragraph | x2.0 | Same, applied at paragraph breaks. |
| Extra ms per letter | 0 | Adds delay to long words. |
| Letter delay threshold | 5 | Word length above which the per-letter delay kicks in. |
| Font size | 56 sp | Focal word size. |
| Font family | Default | Default / Serif / Sans-serif / Monospace. |
| Bionic mode | Off | Off / Main only / Context only / Both. |
| Theme | Light Phone | System / Light / Dark / Light Phone (true black). |
| Input mode | Hold zones | Hold zones / Swipe / Zone swipe. |
| Show zone guides | Off | Draws hairlines along every gesture boundary. Off by default. |
| Swipe distance per word | 10 dp | Only used in swipe-based input modes. |
app/src/main/java/com/fastread/
├── MainActivity.kt # NavHost + theme wiring
├── data/ # Book, Settings, repositories (SharedPreferences + JSON)
├── hw/ # Light Phone III wheel: key recognition + notch bus
├── parser/ # EPUB, MOBI, HTML strip — all hand-written
└── ui/
├── home/ # Library list + SAF importer
├── reader/ # Gesture surface + RSVP loop
├── settings/ # All configurable values
└── theme/ # Material 3 theme + dynamic color
- UI: Jetpack Compose + Navigation Compose. Three destinations:
home,reader/{bookId},settings. - Persistence: SharedPreferences for the book list and settings (kotlinx.serialization JSON). Each imported book's extracted text is cached as a plain
.txtfile in the app's internalfiles/books/directory. - Parsers:
EpubParser— reads the ZIP, locates the OPF viaMETA-INF/container.xml, walks the spine, strips HTML.MobiParser— parses the PDB header, decompresses PalmDOC (type 2) or reads uncompressed (type 1), strips HTML.HtmlStripper— minimal tag remover with entity decoding; sized for ebook body text.
- Progress:
currentWordIndexper book is debounce-saved on change and on screen dispose, so you never lose your place.
Compose BOM, Navigation Compose, Material 3 + extended icons, Lifecycle, AndroidX Activity, AndroidX DocumentFile, kotlinx-serialization-json. That's it. No analytics, no crash reporters, no networking libraries, no ebook SDKs.
- No internet permission. Check
AndroidManifest.xml. - No analytics, no telemetry, no crash reporting.
- No accounts, no sign-in.
- Book contents never leave the device — they live in the app's internal files directory and are removed when you uninstall.
Could land later (PRs welcome):
- ORP (Optimal Recognition Point) focal-letter highlighting.
- More formats: plain
.txt,.fb2, PDF text extraction. - Library sorting / search / collections.
- Export reading stats (locally).
Intentionally out of scope:
- Cloud sync, accounts, library sharing.
- Rich rendering (images, formatting, footnotes).
- HUFF/CDIC-compressed MOBI (rare in practice; surfaces a friendly error).
- DRM-protected ebooks.
Bug reports and PRs are welcome. A few notes:
- Keep dependencies lean. New libraries should be justified.
- Match the existing Compose / repository style; no DI framework, no architectural ceremony.
- For reader-loop changes, test at both extremes: very slow (under 100 WPM) and very fast (over 1000 WPM). Smoothness at the edges matters.
- See
CLAUDE.mdfor a concise tour of the codebase aimed at newcomers (and AI assistants).
Twelve tools for the Light Phone III, all open source, all built in one run.
| Tool | What it does | Built on |
|---|---|---|
| LightPass | Photograph a movie ticket, keep the stub | Plain Android |
| LightQR | QR scanner, plus a browser generator | Plain Android |
| LightRSS | RSS and Atom reader with images and QR subscribe | light-sdk, fork of zachattack323/LightRSS |
| LightNYCSubway | Live MTA subway arrivals | light-sdk fork |
| chat | iMessage over a self-hosted BlueBubbles server | Fork of craigeley/chat |
| LightFog | Fog of World companion, GPS recorder and fog map | Fork of garado/light-topographic |
| LightNonogram | Picross, plus a generator that only ships solvable puzzles | Kotlin generator, light-sdk tool |
| LightSolitaire | Klondike, draw one, unlimited redeals | light-sdk |
| LightFastread (this repo) | RSVP speed reader for EPUB and MOBI | Fork of fluffyspace/FastRead |
| LightTip | Tip calculator, plus a receipt splitter that reads the line items | Plain Android |
| LightNoise | Twelve synthesized sounds, a two-layer mixer and a sleep timer | Plain Android |
| LightPods | AirPods battery, in-ear and lid status | Plain Android, ports LibrePods |
The Light Phone does not sponsor or endorse any of these. Licences vary per repo. This repo is GPL-3.0, unlike the rest of the collection, because upstream FastRead is.
FastRead is released under the GNU General Public License v3.0 (GPL-3.0). See LICENSE for the full text.
In short: you are free to use, study, modify, and redistribute the app, but any distributed derivative work must also be released under GPL-3.0 with source available. This keeps FastRead and its forks open to the community.
The reading engine, both parsers and the whole gesture design belong to fluffyspace, in fluffyspace/FastRead. This fork contributes a display treatment for one piece of hardware and nothing else. Thank you.
queueingqt/FastRead is a fork of that original, not the source. An earlier version of this file credited it by mistake.
RSVP as a reading technique has been around for decades; FastRead's contribution is a tight, gesture-first Android implementation that respects your privacy and your library.



