# LearnLayer Desktop (macOS)

The Why? / How? / Teach Me / Quiz Me / Make Me Do One buttons for **any app on your Mac**:
Claude desktop, ChatGPT desktop, Safari, Preview PDFs, anywhere text can be selected. A small
always-on-top panel talks to the same live LearnLayer engine as the extension, web app, and CLI,
with the same privacy promise and the same shared 10-a-day free tier (it reuses the CLI's
`~/.learnlayer-device` id, so desktop + terminal count as ONE device, not two).

## Install (one command)

```bash
curl -fsSL https://learnlayer-concept.pages.dev/desktop/install.sh | bash
```

That installs to `~/.learnlayer/desktop/` and writes a double-clickable launcher at
`~/Applications/LearnLayer.command`. No sudo, no packages installed, no build step. It needs a
`python3` that can `import tkinter` (Homebrew Python has it; if yours does not, the installer
tells you the one-line fix: `brew install python-tk`).

From a repo checkout instead: `bash learnlayer/desktop/install.sh` (copies local files).

**Uninstall:** `curl -fsSL https://learnlayer-concept.pages.dev/desktop/install.sh | bash -s uninstall`
(removes `~/.learnlayer/desktop` and the launcher; leaves the shared `~/.learnlayer-device`
free-tier id alone in case the CLI still uses it).

## How you use it (two flows, honest about permissions)

### Flow 1: the simple flow. Works immediately, needs NO permissions.
1. Double-click `~/Applications/LearnLayer.command`. The LearnLayer panel opens, always on top.
2. In any app, select the AI answer you were about to copy into your homework. Press **Cmd+C**
   (you press it; nothing is simulated).
3. Click **"Use copied text"** in the panel, then one of the five mode buttons.
4. The lesson renders in the panel. **Esc** hides the panel; **Copy lesson** copies it out.

Reading the clipboard requires no macOS permission, so this flow can never break on a
permissions change. It is the default and it is enough.

### Flow 2: the global hotkey upgrade (Cmd+Shift+L). Needs one Accessibility toggle.
With Accessibility granted, pressing **Cmd+Shift+L in any app** makes LearnLayer copy your
current selection for you (it simulates Cmd+C, then puts your old clipboard back exactly as it
was), pop the panel, and wait for a mode click. One keystroke instead of Cmd+C + click.

**The exact toggle:** System Settings → **Privacy & Security** → **Accessibility** → click
**"+"** → add the app that hosts LearnLayer's process. If you launch via the `.command`
double-click, that host is **Terminal** (`/System/Applications/Utilities/Terminal.app`); if you
run it from iTerm, add iTerm. Flip the toggle ON, then quit and relaunch LearnLayer. The panel's
hint line always says which flow is active; from a terminal you can also probe it:

```bash
python3 ~/.learnlayer/desktop/learnlayer_desktop.py --check
```

Why this permission: macOS only lets a process listen for global keystrokes (the hotkey) and
post synthetic ones (the Cmd+C) if the user grants Accessibility. That is the whole reason the
permission exists, and it is why the simple flow, which does neither, needs nothing.

## Privacy

Identical to every other LearnLayer surface: the ONLY network request happens when you click a
mode button, and it sends exactly the text you captured (plus the question, if your selection's
first line looked like one), once, to generate the lesson. Nothing is stored server-side. The
clipboard save/restore in Flow 2 happens entirely on your Mac. The one identifier is the random
`ll_` device id in `~/.learnlayer-device` used for the daily free counter.

## What is tested vs. what needs Matt (honest status)

**Proven headless (42-check selftest, `python3 test_desktop.py`, all passing):**
- Lesson fetch against a mocked endpoint: happy path, 429, `limit_reached`, server errors,
  garbage JSON, transport failure, client-side prompt/response caps, device-id header.
- Device-id file logic (create, reuse, invalid-regenerate, 0600 mode, unwritable-home fallback).
- Clipboard save/restore (`grab_selection`) with a fake pasteboard: capture + restore, the
  no-selection case leaves the clipboard untouched, restore failure never raises.
- Lesson rendering for all 5 modes, including empty/malformed lessons.
- Selection splitting (question-first heuristic, conservative default).

**Proven live (this machine, 2026-07-08):**
- A real `fetch_lesson` call against the production `/learnlayer` endpoint returned a correct
  lesson with `remaining: 7`. (Found + fixed in the process: Cloudflare 403s python-urllib's
  default User-Agent; the client now sends `learnlayer-desktop/0.1`.)
- The `--check` probe (mac APIs present, Accessibility state, device id) runs clean.

**UNTESTED, needs Matt (~2 minutes):** the live hotkey-in-another-app flow. Creating a Quartz
event tap and posting synthetic Cmd+C require the Accessibility grant, which only a human can
click, so no autonomous session can honestly verify it. Also untested: the panel's on-screen
look. The build session confirmed the window server never PRESENTS windows created from a
sandboxed agent process (Tk reports viewable=1, Quartz lists 0 on-screen windows), so all panel
logic was driven programmatically (capture, render, copy paths all pass) but no honest
screenshot of the visible panel exists yet. Launched normally from Terminal or Finder it will
present; grab the 5th store screenshot during the test below.

**Matt's 2-minute test script:**
1. `bash learnlayer/desktop/install.sh` (from the repo; or the curl one-liner).
2. Double-click `~/Applications/LearnLayer.command`. Panel opens. In Safari or Claude desktop,
   select any paragraph, press Cmd+C, click **Use copied text**, click **Why?**. A lesson should
   render within ~10 s. That validates Flow 1 end to end.
3. System Settings → Privacy & Security → Accessibility → + → Terminal → toggle ON. Quit the
   panel (close the Terminal window it lives in), relaunch the `.command`. The hint line should
   now mention Cmd+Shift+L.
4. In Claude desktop, select an answer, press **Cmd+Shift+L**. The panel should pop with
   "Captured N chars", and your clipboard should still hold whatever it held before. Click
   **Quiz Me**. That validates Flow 2 end to end.
5. If anything misfires, run `~/.learnlayer/desktop/learnlayer_desktop.py --check` with the
   python from the `.command` and read the panel's hint line: they name the active flow.
6. Optional 30 extra seconds: with a lesson showing, press Cmd+Shift+4 then Space and click the
   panel; save the shot as `learnlayer/store-assets/desktop-panel.png`. That is the honest 5th
   store screenshot no autonomous session could take (see the untested note above).

## Windows / Linux (what would change)

The core (`lldcore.py`) is already OS-neutral: fetch, render, device id, and the
`grab_selection` orchestration are pure Python with injected adapters. Porting means swapping
the two macOS adapters in `learnlayer_desktop.py`:
- **Clipboard:** Windows `ctypes` user32/kernel32 or `powershell Get-Clipboard`; Linux
  `xclip`/`wl-paste`. (tkinter itself can also read clipboards portably.)
- **Hotkey + simulated copy:** Windows `RegisterHotKey` + `SendInput` via ctypes (no permission
  prompt at all on Windows); Linux X11 `XGrabKey`/xdotool (Wayland makes global hotkeys a
  compositor setting instead).
- The tkinter panel runs unchanged on both. The installer would become a `.ps1`/`sh` variant.
Not built; macOS is the only shipped target today.

## Files

```
desktop/
  lldcore.py             OS-neutral core: fetch_lesson, render_lesson_text, get_device_id,
                         grab_selection (injected clipboard/keys), split_selection. Unit-tested.
  learnlayer_desktop.py  The app: tkinter panel + NSPasteboard clipboard + Quartz event-tap
                         hotkey (Accessibility-gated, graceful fallback) + --check probe.
  test_desktop.py        42-check headless selftest (fake pasteboard, mocked endpoint).
  install.sh             curl-able installer/uninstaller; writes ~/Applications/LearnLayer.command.
  README.md              This file.
```
