Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs (Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet (marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert (Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen, a öffnet die Liste (springen, Notiz ändern, löschen). Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt. Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf (tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der Server-Smoke schreibt und mischt zwei Geräte. 75 Tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
191 lines
11 KiB
Markdown
191 lines
11 KiB
Markdown
# diora-tui
|
||
|
||
EPUB-Reader im Terminal — die erste Stufe einer TUI-Version von diora.
|
||
|
||
Zeigt ein Buch als **eine durchgehende Ansicht** über alle Kapitel hinweg (wie diora's
|
||
Web-Reader), nicht Kapitel für Kapitel. Lesefortschritt wird als dieselbe
|
||
`"blockIndex:innerFraction"`-Positionsangabe geführt wie der Web-Reader (`books/models.py`,
|
||
`EBookProgress`) — `blockIndex` zählt dabei exakt wie `static/js/app.js`s
|
||
`EPUB_BLOCK_SELECTOR` (`p, h1-h6, li, blockquote, dt, dd, figcaption` + textbasierte
|
||
`div`s ohne Element-Kinder), fortlaufend über das ganze Buch. Dadurch ist die Position
|
||
zwischen TUI und Web-Reader direkt vergleichbar, und `diora-tui sync` kann Fortschritt in
|
||
beide Richtungen synchronisieren (siehe unten).
|
||
|
||
Die Lesestelle folgt dem Vertrag aus `docs/progress-sync.md`: **die zuletzt gelesene Stelle
|
||
gewinnt** (auf welchem Gerät auch immer), die weiteste bleibt als Hinweis erhalten. Die
|
||
Regeln stehen in `progress.py` und sind dieselben wie auf dem Server und in den Apps.
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
cd tui
|
||
pip install -e .
|
||
```
|
||
|
||
## Nutzung
|
||
|
||
```bash
|
||
diora-tui --library ~/Books # Standard: ~/Books
|
||
```
|
||
|
||
Tastenkürzel:
|
||
|
||
- `↑`/`k`, `↓`/`j` — zeilenweise scrollen
|
||
- `n` — nächstes Kapitel, `p` — vorheriges Kapitel
|
||
- `w` — zur weitesten Stelle des Buchs springen (auf Wunsch; beim Öffnen weist ein Hinweis darauf hin)
|
||
- `v` — Text markieren (siehe unten), `b` — Lesezeichen setzen, `a` — Liste der Lesezeichen,
|
||
Markierungen und Notizen
|
||
- `f` — Fußnote in der Nähe der aktuellen Position anzeigen (Peek-Overlay, `Escape`/`f`/`q`
|
||
zum Schließen); Erkennung folgt derselben Heuristik wie `app.js`s
|
||
`_looksLikeFootnoteLink` (Link in/um `<sup>`, Klassenname mit note/footnote/fn, oder
|
||
`epub:type="noteref"`)
|
||
- Bibliothek: `Enter` öffnet das markierte Buch, `c` das Buch der „Weiterlesen“-Karte, `1`–`4`
|
||
Status (Alle · Angefangen · Ungelesen · Gelesen), `o` Ordner, `/` Suche, `s` Sortierung,
|
||
`m` Bücher ⇄ Zeitschriften, `Escape` setzt Suche und Filter zurück. Chips und Karte lassen
|
||
sich auch anklicken.
|
||
- `Escape` / `q` — zurück zur Bibliothek (im Reader) bzw. beenden (in der Bibliothek)
|
||
|
||
Der Fließtext ist auf 120 Zeichen Breite begrenzt und horizontal zentriert (lesbarer als
|
||
volle Terminalbreite bei breiten Fenstern).
|
||
|
||
Unten rechts zeigt eine Statusleiste Akkustand (falls vorhanden, via `psutil`) und Uhrzeit,
|
||
sekündlich aktualisiert, auf Bibliotheks- und Reader-Ansicht.
|
||
|
||
## Die Bibliothek
|
||
|
||
Wie die Bibliothek im Web (`docs/library.md`), nur im Terminal:
|
||
|
||
- **Weiterlesen-Karte** oben (Titel, Autor, Balken, „Seite 132 von 186“, wann und auf welchem
|
||
Gerät zuletzt gelesen) und darunter die Bücher, die davor gelesen wurden. Das Buch der Karte
|
||
steht nicht noch einmal in der Liste. Bei Suche und Filtern entfällt die Karte.
|
||
- **Chips:** Alle · Angefangen · Ungelesen · Gelesen (mit Anzahl), dann die Ordner in der
|
||
Reihenfolge, die im Web eingestellt ist (verschlüsselte Bibliothekseinstellungen), und
|
||
„Ohne Ordner“. Ein Buch kann in mehreren Ordnern stehen.
|
||
- **Suche** in Titel und Autor, ohne Beachtung von Groß-/Kleinschreibung und Akzenten.
|
||
- **Sortierung:** Zuletzt · Titel · Autor · Neu hinzugefügt · Kürzeste/Längste zuerst (Zahlen
|
||
nach Wert: „Band 2“ vor „Band 10“). Sie bleibt gespeichert.
|
||
- **Länge:** die Seitenzahl aus den Metadaten (`pages`); fehlt sie, rechnet die TUI sie für
|
||
schon geladene Bücher einmal aus dem Text aus (1800 Zeichen je Seite, wie der Web-Reader), wenn
|
||
nach Länge sortiert wird („Länge wird ermittelt …“). Nicht geladene Bücher ohne Länge stehen am
|
||
Ende. Zurück auf den Server schreibt die TUI die Länge nicht; das macht der Web-Reader.
|
||
- **Bücher ⇄ Zeitschriften** (`m`): Zeitschriften sind, was in den Metadaten so markiert ist
|
||
oder im Ordner „Zeitschriften“/„Magazine“ liegt. Die Heft-Ansicht mit Artikeln gibt es in der
|
||
TUI nicht; ein Heft ist ein Buch.
|
||
- ☁ vor dem Titel: auf dem Server, noch nicht geladen. ✓: gelesen.
|
||
|
||
Nicht in der TUI: Serien-Stapel, Ausgaben (mehrere Fassungen eines Werks stehen einzeln),
|
||
Cover, Ordner anlegen/zuweisen, Eingang aus der Cloud.
|
||
|
||
## Markierungen, Notizen, Lesezeichen
|
||
|
||
Dieselben Daten wie im Web-Reader (verschlüsselt mit dem Buchschlüssel, je Buch und Art eine Liste
|
||
`/books/<id>/highlights/` und `/bookmarks/`; Format in `annotations.py`). Was im Web, in den Apps
|
||
oder hier gemacht wird, erscheint überall.
|
||
|
||
- **Anzeige:** Markierungen stehen im Text mit der Farbe (Gelb, Grün, Blau, Rot); eine freie Notiz
|
||
ist unterstrichen. Die Anker des Web-Readers (Block-Nummer plus Zeichenposition im Rohtext des
|
||
Absatzes) werden auf den angezeigten Text umgerechnet; alte Anker ohne Blocknummer findet die TUI
|
||
wie das Web über das Zitat. `a` öffnet die Liste (Enter springt dorthin, `e` ändert die Notiz,
|
||
`d` löscht, Escape schließt).
|
||
- **Markieren (`v`):** Der Cursor steht auf einem Wort; `h`/`l` wechseln das Wort, `j`/`k` den
|
||
Absatz. Enter setzt den Anfang, danach `h`/`l`/`j`/`k` zum Ende und Enter: Farbe wählen, oder
|
||
„Gelb mit Notiz“. `n` legt statt dessen eine Notiz auf das Wort bzw. die Auswahl. Escape bricht
|
||
ab. Stellen, die sich nicht genau zuordnen lassen (ein Fußnotenmarker mit Leerraum im Absatz),
|
||
lehnt die TUI ab, statt eine falsche Position zu schreiben.
|
||
- **Lesezeichen (`b`):** wie im Web das aktuelle Kapitel (Name der Kapiteldatei, neueste zuerst).
|
||
- **Abgleich:** Der Server ersetzt die Liste als Ganzes und führt nichts zusammen; die Apps
|
||
schreiben deshalb ihre ganze Liste. Die TUI merkt sich, was sie zuletzt vom Server gesehen hat, und
|
||
legt vor dem Schreiben ihre Änderungen (neu, gelöscht, geändert, je Eintrag-ID) auf die aktuelle
|
||
Liste des Servers: was ein anderes Gerät inzwischen markiert hat, geht nicht verloren. Offline
|
||
bleiben Änderungen vorgemerkt und gehen beim nächsten Abgleich, beim Verlassen des Buchs oder
|
||
beim Beenden hinaus. Beim Öffnen eines Buchs werden seine Listen geholt.
|
||
- **Prüfung gegen das Web:** `tools/parity/web_anchors.cjs` lässt den echten `resolveAnchorRange`
|
||
des Web-Readers die in der TUI erzeugten Anker auflösen (an zwei echten Büchern: alle 81 Anker
|
||
an der gemeinten Stelle); umgekehrt lassen sich 34 von 34 echten Web-Markierungen anzeigen (eine alte
|
||
XPath-Notiz über ihr Zitat). Nicht in der TUI: PDF-Markierungen.
|
||
|
||
## Anmelden und synchronisieren
|
||
|
||
```bash
|
||
diora-tui login # einmal; fragt Benutzername und Passwort
|
||
diora-tui # die Oberfläche, synct beim Start und beim Beenden
|
||
diora-tui sync # nur abgleichen, ohne Oberfläche
|
||
diora-tui logout # gespeicherte Zugangsdaten löschen
|
||
```
|
||
|
||
`login` meldet sich wie die Apps über das Web-Formular an, liest den vorhandenen API-Token
|
||
von der Einstellungsseite (es wird nie ein neuer erzeugt: das würde die Apps abmelden) und
|
||
leitet den Buchschlüssel aus dem Passwort ab (PBKDF2, wie „Mit Passwort entsperren“ im Web).
|
||
Das Passwort wird nicht gespeichert. Passt der abgeleitete Schlüssel nicht (Passwort seit dem
|
||
Hochladen geändert, Schlüssel zufällig erzeugt), fragt `login` nach dem Schlüssel
|
||
(diora: Einstellungen → Schlüssel kopieren; `--paste-key` fragt gleich danach).
|
||
|
||
Token und Schlüssel liegen geschützt in `config.json`: unter Windows per DPAPI (nur dieser
|
||
Windows-Benutzer auf diesem Rechner kann sie lesen), sonst als Base64 in einer 0600-Datei.
|
||
Eine ältere Konfiguration mit Klartextfeldern wird beim ersten Lesen geschützt neu geschrieben.
|
||
Alle Dateien liegen unter `platformdirs`' Verzeichnissen; `DIORA_TUI_HOME=<Ordner>` legt sie
|
||
stattdessen gemeinsam in einen Ordner (zweites Konto, Tests).
|
||
|
||
**Was synchronisiert wird:** die Bücherliste (`GET /books/`, ohne die Blobs der Markierungen)
|
||
samt Lesestand, Gelesen-Status und der Uhr des Servers. Die Bücher selbst werden erst beim
|
||
Öffnen geladen und entschlüsselt (`<id> - <Titel>.epub` im Bibliotheksordner). PDFs werden
|
||
übersprungen.
|
||
|
||
**Lesestand** (`progress.py`, `reader_screen.py`):
|
||
|
||
- Gespeichert wird nur, was der Leser selbst bewegt hat (Tasten, Mausrad), mit dem Zeitpunkt
|
||
des Ankommens (`read_at`). Wiederherstellen, Umbruch bei Größenänderung und Sprünge auf
|
||
Angebote ändern nichts. Ein offenes, ruhendes Buch kann deshalb nie eine neuere Stelle von
|
||
woanders überschreiben.
|
||
- Zeiten sind Serverzeit: die TUI lernt aus jeder Antwort den Versatz ihrer Uhr, und der
|
||
Server korrigiert mit `sent_at` nach. Ein falsch gehender Rechner ordnet trotzdem richtig.
|
||
- Jede Stelle wird lokal angewendet und vorgemerkt (`pending`, je Buch die später gelesene),
|
||
bis der Server sie hat, auch offline; nach einem Abgleich wird sie nach derselben Regel auf
|
||
den Stand des Servers gelegt.
|
||
- Alle zwei Minuten fragt ein offenes Buch den Server. Liegt dort eine neuere Stelle von einem
|
||
anderen Gerät, fragt die TUI „Auf *X* weitergelesen — bei n %. Springen?“ (`y`/`n`), und
|
||
bewegt sich nie von allein.
|
||
- Der Fortschritt in % ist wie im Web der Anteil am Buchtext in Zeichen
|
||
(`blocks.fraction_for_anchor`), nicht mehr 0.
|
||
|
||
## Performance bei großen Büchern
|
||
|
||
Sehr große Bücher (mehrstellige Tausend Absätze — z.B. Sammelbände) können beim
|
||
*ersten* Öffnen mehrere Sekunden bis niedrige zweistellige Sekunden brauchen (Parsing +
|
||
Zeilenumbruch-Berechnung für die durchgehende Ansicht). Ein zweites Öffnen desselben
|
||
Buchs bei gleicher Terminalbreite ist dank Cache (`~/.cache/diora-tui/layout_cache/`)
|
||
deutlich schneller. Das Rendering selbst skaliert nicht mit der Buchgröße — nur die
|
||
tatsächlich sichtbaren Zeilen werden gezeichnet (Textual Line API), nicht das ganze Buch
|
||
auf einmal.
|
||
|
||
## Tests und Anker-Parität mit dem Web-Reader
|
||
|
||
```bash
|
||
pip install -e . pytest
|
||
pytest tests # Blockliste, Anker, App-Start (Textual-Pilot)
|
||
```
|
||
|
||
Die Position ist ein Block-Index, den alle Clients gleich zählen müssen. Ob die TUI das tut,
|
||
prüft `tools/parity/` gegen den echten Web-Code (`parseEpub` aus `static/js/app/40-epub.js`
|
||
in headless Chromium):
|
||
|
||
```bash
|
||
NODE_PATH=<diora-web>/node_modules PLAYWRIGHT_BROWSERS_PATH=... node tools/parity/web_blocks.cjs <diora-web> web.json buch1.epub buch2.epub
|
||
python tools/parity/compare_blocks.py web.json
|
||
```
|
||
|
||
Stand 2026-10-26 (6 Bücher, bis 25.798 Blöcke): Anzahl, Tags, Text, Zeichenposition und
|
||
-länge aller Blöcke sowie die Gesamtlänge sind identisch. Das setzt voraus, dass die TUI den
|
||
EPUB-Inhalt wie der Browser liest: UTF-8-dekodiert, mit dem HTML5-Parser (`html5lib`, nicht
|
||
libxml2) und aus den Rohdaten (`item.content`; `get_content()` von ebooklib formatiert um und
|
||
fügt Leerraum ein).
|
||
|
||
## Grenzen der aktuellen Version
|
||
|
||
- Nur EPUB, kein PDF (der Abgleich überspringt PDFs und meldet sie); keine PDF-Markierungen.
|
||
- Notizen sind einzeilig; mehrzeilige Notizen aus dem Web werden angezeigt und lassen sich ersetzen.
|
||
- Text wird als Fließtext ohne Bild-/Layout-Rendering dargestellt.
|
||
- `innerFraction` (die Position *innerhalb* eines Blocks) ist eine Terminal-Näherung
|
||
(zeilenbasiert statt pixelbasiert wie im Browser) — für die Fortschritts-Sortierung
|
||
zählt primär `blockIndex`, der exakt mit dem Web-Reader übereinstimmt; `innerFraction`
|
||
ist nur ein Tiebreaker innerhalb desselben Blocks.
|