|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Build Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# Development with watch mode |
| 9 | +npm run dev |
| 10 | + |
| 11 | +# Production build (runs TypeScript check + esbuild) |
| 12 | +npm run build |
| 13 | + |
| 14 | +# Version bump (updates manifest.json and versions.json) |
| 15 | +npm run version |
| 16 | +``` |
| 17 | + |
| 18 | +**Important**: The build output `main.js` MUST be tracked in git for Obsidian plugin releases. It's intentionally not in .gitignore. |
| 19 | + |
| 20 | +## Architecture Overview |
| 21 | + |
| 22 | +### Plugin Structure |
| 23 | + |
| 24 | +DashReader is an Obsidian plugin implementing RSVP (Rapid Serial Visual Presentation) speed reading. The architecture follows a clear separation: |
| 25 | + |
| 26 | +- **main.ts** - Plugin entry point, registers commands, ribbon icons, and manages view lifecycle |
| 27 | +- **src/rsvp-view.ts** - UI layer (ItemView), handles user interactions, cursor tracking, and display |
| 28 | +- **src/rsvp-engine.ts** - Core reading engine, controls timing, word iteration, and micropause logic |
| 29 | +- **src/markdown-parser.ts** - Transforms Markdown to plain text while marking headings with `[H1]`, `[H2]` etc. |
| 30 | +- **src/settings.ts** - Settings UI using Obsidian's PluginSettingTab |
| 31 | +- **src/types.ts** - Shared interfaces and default settings |
| 32 | + |
| 33 | +### Key Architecture Patterns |
| 34 | + |
| 35 | +**View-Engine Separation**: The view (`rsvp-view.ts`) owns the UI and event handling, while the engine (`rsvp-engine.ts`) owns reading logic and timing. They communicate via: |
| 36 | +- View → Engine: `setText()`, `play()`, `pause()`, `updateSettings()` |
| 37 | +- Engine → View: `onWordChange` callback, `onComplete` callback |
| 38 | + |
| 39 | +**Cursor Position Tracking**: When loading text from the editor: |
| 40 | +1. Parse Markdown FIRST to remove syntax (`markdown-parser.ts`) |
| 41 | +2. Parse text up to cursor position separately |
| 42 | +3. Count words in parsed text (not raw Markdown with frontmatter) |
| 43 | +4. Pass word INDEX to engine, not character position |
| 44 | + |
| 45 | +**Heading System**: Headings are marked during parsing (`# Title` → `[H1]Title`), then: |
| 46 | +- View detects markers and displays with proportional font size (H1=2x, H2=1.75x, etc.) |
| 47 | +- View adds visual separator lines before headings |
| 48 | +- Engine applies longer micropauses (H1=3x, H2=2.5x, etc.) |
| 49 | + |
| 50 | +**Accurate Time Estimation**: `getEstimatedDuration()` and `getRemainingTime()` iterate through ALL remaining words and sum their individual delays, accounting for: |
| 51 | +- Heading micropauses |
| 52 | +- Punctuation pauses |
| 53 | +- Long word pauses |
| 54 | +- Section markers (numbers, bullets) |
| 55 | +- Progressive acceleration (average of start and target WPM) |
| 56 | + |
| 57 | +### Markdown Parsing Order |
| 58 | + |
| 59 | +The parser (`markdown-parser.ts`) processes in this specific order: |
| 60 | +1. Extract frontmatter (remove it entirely) |
| 61 | +2. Extract code blocks (keep content, remove delimiters) |
| 62 | +3. Mark headings with level tags |
| 63 | +4. Remove formatting (bold, italic, highlights) |
| 64 | +5. Process links (keep text, remove URLs) |
| 65 | +6. Handle callouts |
| 66 | +7. Clean up extra whitespace |
| 67 | + |
| 68 | +**Critical**: Always parse BEFORE counting words for cursor positioning. |
| 69 | + |
| 70 | +### Obsidian Integration Points |
| 71 | + |
| 72 | +- **View Type**: Custom ItemView with `VIEW_TYPE_DASHREADER = 'dashreader-view'` |
| 73 | +- **Leaf Management**: View is activated via `this.app.workspace.getRightLeaf(false)` |
| 74 | +- **Editor Events**: Listens to `active-leaf-change`, `file-open`, `mouseup`, `keyup` with throttling |
| 75 | +- **Context Menu**: Adds "Read with DashReader" when text is selected |
| 76 | + |
| 77 | +### Hotkey System |
| 78 | + |
| 79 | +**Important**: Only `Shift+Space` triggers play/pause (not Space alone). This prevents capturing Space when typing in notes. |
| 80 | + |
| 81 | +Other hotkeys from settings: |
| 82 | +- `hotkeyRewind/hotkeyForward` - Navigation |
| 83 | +- `hotkeyIncrementWpm/hotkeyDecrementWpm` - Speed control |
| 84 | +- `hotkeyQuit` - Stop reading |
| 85 | + |
| 86 | +### Settings Architecture |
| 87 | + |
| 88 | +Settings are defined in `src/types.ts` as: |
| 89 | +- Interface: `DashReaderSettings` |
| 90 | +- Defaults: `DEFAULT_SETTINGS` const |
| 91 | + |
| 92 | +UI is built in `src/settings.ts` using Obsidian's Setting API. Inline settings in the view mirror the main settings tab. |
| 93 | + |
| 94 | +### Micropause System |
| 95 | + |
| 96 | +Micropauses multiply the base delay (`60/WPM * 1000 ms`). Multiple conditions can stack multiplicatively: |
| 97 | + |
| 98 | +```typescript |
| 99 | +// Example: H1 heading with period and long word |
| 100 | +multiplier = 3.0 (H1) * 1.5 (period) * 1.3 (>8 chars) = 5.85x delay |
| 101 | +``` |
| 102 | + |
| 103 | +Order of detection in `calculateDelay()`: |
| 104 | +1. Headings (`[H1]` through `[H6]`) |
| 105 | +2. Section markers (1., I., etc.) |
| 106 | +3. List bullets (-, *, +, •) |
| 107 | +4. Punctuation (end of word) |
| 108 | +5. Long words (>8 characters) |
| 109 | +6. Paragraph breaks (`\n`) |
| 110 | + |
| 111 | +## Release Process |
| 112 | + |
| 113 | +For Obsidian plugin submission: |
| 114 | + |
| 115 | +1. Update `manifest.json` version (must match git tag exactly, no `v` prefix) |
| 116 | +2. Run `npm run build` |
| 117 | +3. Commit changes including `main.js` |
| 118 | +4. Create GitHub release with tag matching manifest version (e.g. `1.3.0`) |
| 119 | +5. Attach `main.js`, `manifest.json`, `styles.css` as binary assets to the release |
| 120 | + |
| 121 | +The manifest.json at repo root is used by Obsidian to check version. Actual files are fetched from GitHub release assets. |
| 122 | + |
| 123 | +## Testing in Obsidian |
| 124 | + |
| 125 | +```bash |
| 126 | +# Install in vault (creates symlink) |
| 127 | +./install-local.sh /path/to/vault |
| 128 | + |
| 129 | +# Or manually copy after build |
| 130 | +cp main.js manifest.json styles.css /path/to/vault/.obsidian/plugins/dashreader/ |
| 131 | + |
| 132 | +# Reload Obsidian |
| 133 | +# macOS: Cmd+R |
| 134 | +# Windows/Linux: Ctrl+R |
| 135 | +``` |
| 136 | + |
| 137 | +## Code Conventions |
| 138 | + |
| 139 | +- **Console logging**: Prefix with `DashReader:` for easy filtering |
| 140 | +- **Word index vs position**: Always use word index (count) after parsing, never character position from raw text |
| 141 | +- **Event throttling**: Cursor tracking uses 150ms throttle to balance responsiveness and performance |
| 142 | +- **Styling**: Use CSS variables for theme compatibility (`var(--text-muted)`, etc.) |
0 commit comments