diora-web/tui/README.md
mrwnslz e246a32212 TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
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>
2026-10-26 16:42:32 +01:00

11 KiB
Raw Blame History

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.jss EPUB_BLOCK_SELECTOR (p, h1-h6, li, blockquote, dt, dd, figcaption + textbasierte divs 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

cd tui
pip install -e .

Nutzung

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.jss _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

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

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):

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.