PeakRead

A tiny reader that floats over your screen. Press a key, scroll to read, press Esc. Your work stays exactly where it was.

Coming Q1 2027 macOS 14+ · menu-bar app
  1. scroll ↓

    Read why I made it.

  2. open

    Press ⌘⌥R. It pops up.

  3. read

    Scroll to read. One scroll, one line.

  4. finish

    No timer to race. It just ends.

  5. close

    Press Esc. Your work is still there.

Read in the gaps.

It took five tries.

PeakRead took five tries. One: reading while you work fails, because your brain can't read and write at once. Two: tracking your head fails, because your head is not your eyes. Three: a timer feels stressful. Four: the scroll wheel worked, but books are not the only thing you read. Five: keep books, and add articles, tabs and newsletters. Two minutes, four lines, just scroll.

Nothing to restore.

PeakRead is a popup that never becomes your active app. It never takes your keyboard, so you keep typing in your editor. It floats above full-screen apps. Your mouse is locked where it was. Scrolling moves the reader, not your page. Press Esc and it is gone, with nothing to put back.

One catch: the mouse lock and the scroll capture need Accessibility permission. Without it, the popup still shows, but those two don't work.

Try it

Read something you saved.

Thirty seconds, one screen of text. Pick a sample or paste your own, click summon, then scroll. While it's up the wheel belongs to the reader, like the app. Esc gives it back.

reader is dismissed — your work is untouched

⌘⌥R
Summon or dismiss, from anywhere
Scroll
Forward one line per notch. Back one line, instantly.
Right-click
Session length, 30 s to 30 min. Scroll to change; the clock pauses while you fiddle.
⌥Scroll
Reader size, 80–300 %. Remembered next time. (here: 80–200 %)
Double-click
Contents, built from the book's own chapters. See it in the next section.

Straight to page one.

Books start with pages you don't want to read: a title page, copyright, a newsletter promo, a dedication, an also-by list. PeakRead skips them, so you open at the prologue, and it only ever trims the start, never more than a quarter of the book. Double-click to see a table of contents built from the book's own chapters and acts, scroll to pick one, double-click again to jump there.

Extras

And the small things.

Little things you only notice when they're missing.

Today's reading.

Words and sessions, added up across every book. Fresh again at midnight.

1,240words read
3sessions
2books

Keeps your place.

Close it. Come back tomorrow. Same book, same line.

saved as you go

Your length.

Right-click to set a session. Thirty seconds to thirty minutes.

2:00scroll to change

Your size.

Hold ⌥ and scroll. Small on a laptop, huge on a big screen.

Waits for the full stop.

If time runs out mid-sentence, it lets you finish the sentence first. No cut-offs.

Numbers are samples.

Release

Planned for

Q1 2027

When it's ready, it's ready.

  1. The idea
  2. It works
  3. Betawe are here
  4. ReleaseQ1 2027

Where things stand. PeakRead already works. The floating reader, the scroll wheel, the frozen mouse, books with the junk skipped and the contents found, your stats and your place. It's all built, and you can see how it works higher up this page.

Why beta. We're still building it, polishing it and finding what breaks. Some things will change, and we're adding more to read on top of books.

What you can do now. Try the reader above. There's no download and nothing to install yet. Leave your email below and I'll send one note when it's ready.

Tell me when it's ready.

One email at launch. Nothing else.

You'll get a confirmation email first. Unsubscribe any time.

Made for Mac · macOS 14 or later

For the person who reads every line:  · 

Deep dive

What's under the reading.

You don't need this to use PeakRead. It's for the person who reads every line. I didn't run any studies, and I'm not a researcher. I read a lot of what's known about how people read and pay attention, and I built around it. Here is what each choice leans on.

It floats over your work

It never leaves your app.

When you switch tasks, part of your attention stays behind on the old one. Researchers call it attention residue. PeakRead never makes you switch, so there is less to leave behind.

You open it on purpose

It won't read while you write.

Reading words and writing words use the same mental channel, called the phonological loop. Do both at once and they fight each other. So PeakRead never streams in the background. You open it, you read, it closes.

The window is four lines

Four lines.

People can hold only a few things in mind at once. Studies put it at about four chunks. Four lines, roughly 24 words, is my choice and not a lab result. It is small on purpose, so you can hold all of it.

The middle line is bright

One bright line.

Only a tiny spot of your vision is sharp, and your eyes jump from spot to spot as you read. One bright line gives your eyes a clear place to land, and the dim lines around it stay out of the way.

One scroll moves one line

You set the pace.

Flashing words one at a time at a fixed speed tends to hurt understanding and tire the eyes. Readers understand more when they are allowed to look back, so scrolling back moves you back one line. Reading faster also costs understanding, so I don't pretend it doesn't.

Time can run out mid-sentence

It waits for the full stop.

Readers slow down at the end of a sentence to piece the meaning together. Cutting you off right there throws that step away. So when the time is up, PeakRead lets you finish the sentence first.

The clock is hidden

No timer to race.

My first version had a visible countdown. It felt stressful. Related research points the same way: people speed up when they feel rushed, stress goes up, and understanding can slip. This is related work, not a test of PeakRead's own timer.

A session is two minutes

Two minutes.

Attention drifts. Minds wander during reading, even with easy text, and focus on long tasks fades with time. A short, bounded session keeps you ahead of the drift. Two minutes is my number, not a magic one, and you can change it.

Where it's instinct

The frozen pointer and the wheel.

Freezing the mouse and using the scroll wheel as the control are my calls. They felt right when I used them, and I couldn't find research that settles either one. I'd rather tell you than dress it up.

PeakRead is a reading tool, not a medical product, and none of this claims it was tested in a lab. Look up the originals before you quote any of it.

How it's built

How the app works.

You don't need to code to enjoy this. Each part starts with a plain explanation, then shows the real code for anyone who wants it. We're explaining the ideas, not every line.

How do you make a popup that never steals your keyboard?

On a Mac, one window at a time is "the one you're typing into". Normal windows fight to become it. So we ask macOS for a window that politely says no thanks to that job. It's called a non-activating panel.

Think of a sticky note on the outside of a glass window. It's right there in front of you, but the room behind it has no idea it exists. We also lift it above normal windows, and tell macOS it may float over full-screen apps too.

OverlayWindowManager.swift
final class OverlayPanel: NSPanel {
    override var canBecomeKey: Bool { false }    // never takes the keyboard
    override var canBecomeMain: Bool { false }

    convenience init(contentRect: NSRect) {
        self.init(
            contentRect: contentRect,
            styleMask: [.borderless, .nonactivatingPanel],
            backing: .buffered, defer: false)
        level = .statusBar                       // above normal windows
        collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
        hidesOnDeactivate = false
    }
}

How do you stop the mouse from moving?

You can't ask a Mac to freeze the mouse. So we cheat. We install a tiny listener that sees every mouse move before any app does. Each time the mouse tries to move, we put it back exactly where it was, and tell macOS the move never happened.

It's like a friend gently putting your hand back every time it drifts, so quickly you never notice. We also hide the cursor while the reader is up, so you don't see it being held still.

OverlayWindowManager.swift
// listen to every mouse move before any app sees it
guard let tap = CGEvent.tapCreate(
    tap: .cgSessionEventTap, place: .headInsertEventTap,
    options: .defaultTap, eventsOfInterest: mask,
    callback: callback, userInfo: Unmanaged.passUnretained(self).toOpaque()
) else { return }    // no permission, so no lock (the popup still shows)

// each move: put the pointer back, then swallow the event
case .mouseMoved:
    CGWarpMouseCursorPosition(frozenPoint)
    return nil

How does the scroll wheel turn into "one scroll, one line"?

A scroll wheel doesn't send "one notch". It sends lots of tiny nudges, dozens in a single flick. So we keep a bucket. Every nudge drops a little into it. When the bucket is full, that's one line, and we empty it.

The same listener also swallows the scroll, so the editor underneath never moves. That's why your work stays exactly where it was.

OverlayWindowManager.swift
case .scrollWheel:
    let delta = event.getIntegerValueField(.scrollWheelEventDeltaAxis1)
    accumulateScroll(delta: CGFloat(delta))
    return nil                                  // the editor never scrolls

private func accumulateScroll(delta: CGFloat) {
    scrollAccumulator += delta                  // the bucket fills with tiny nudges
    if scrollAccumulator >= scrollStepThreshold {       // 12
        scrollAccumulator = 0
        viewportModel?.stepForward()            // one line down
    } else if scrollAccumulator <= -scrollStepThreshold {
        scrollAccumulator = 0
        viewportModel?.stepBackward()           // one line back
    }
}

Why does it ask for Accessibility permission?

Listening to every mouse move and every scroll on the whole Mac is powerful, so macOS makes you say yes first. That's the Accessibility permission. Without it, the popup still shows, but the mouse lock and the scroll capture don't work.

Here's a small trap we hit. macOS ties your "yes" to the app's digital signature. Sign it fresh every rebuild and it forgets you said yes. So we made a stable local signing identity, and the yes sticks.

README.md
# macOS ties the Accessibility "yes" to the app's signature.
# A stable local identity keeps that yes across rebuilds.
./scripts/create_dev_cert.sh
PEAKREAD_SIGN_IDENTITY="PeakRead Dev" ./build_app.sh

How does it skip the junk at the start of a book?

It looks at the first pages. A real copyright page is short, and it's full of giveaway phrases like "all rights reserved" or "sign up for our newsletter". If a page looks like that, it's skipped, and so is the next one, until the first page that looks like a story.

There's a safety valve. If skipping would remove more than a quarter of the book, the guess was wrong, so it skips nothing. We would rather show you a copyright page than hide your first chapter.

FrontMatterFilter.swift
private static let boilerplateMarkers = [
    "all rights reserved", "©", "isbn", "no part of this",
    "sign up for", "newsletter", "special offers", // …and a few more
]
private static let boilerplateMaxWords = 500   // real boilerplate is short
private static let maxTrimmedShare = 0.25      // never trim more than a quarter

while count < chapters.count - 1, isBoilerplate(chapters[count]) {
    count += 1
}
if Double(trimmed) / Double(total) > maxTrimmedShare { return 0 }

How does it know a prologue from a chapter?

It reads the book's own contents list and sorts each title by the words in it. "Prologue" is a prologue. "Act I" is an act. "Chapter 1" is a chapter.

The fun part is the edge cases. A chapter called "An Act in Time" must not be mistaken for a play's Act. So the rule is a whole word "act" followed by a number, and nothing looser.

EPUBBook.swift
if matches("prologue") { return .prologue }
if matches("epilogue") { return .epilogue }
// Whole-word "Act" + numeral, so "Chapter 3: An Act in Time" stays a chapter.
if lowered.range(of: #"\bact ([ivxlc]+|\d+|one|two|three)\b"#,
                 options: .regularExpression) != nil { return .act }
if lowered.hasPrefix("part ") || lowered.hasPrefix("book ") { return .part }
if matches("chapter") { return .chapter }
return .other

How does it remember your place?

Every step, it saves which line you're on. But writing to disk on every single scroll would be wasteful, so it waits a beat and writes once things go quiet. The word for that is a debounce.

It also keeps today's counters, words and sessions. When the calendar day changes, it quietly resets them.

ReadingStore.swift
if !Calendar.current.isDateInToday(progress.lastSessionDate) {
    progress.totalWordsReadToday = 0          // a new day, fresh counters
    progress.sessionsCompletedToday = 0
}
progress.currentLineIndex = currentLineIndex
progress.totalWordsReadToday += max(0, wordsDelta)
progress.lastSessionDate = Date()

cache[bookId] = progress                      // update memory now…
scheduleDebouncedWrite(bookId: bookId)        // …write to disk in a moment

Code is from the app, shortened, with comments added to help.