Replace shared stdin reader with readline + select for comfortable line editing

Use GNU readline (arrow keys, backspace, history, Ctrl-A/E) for all user
prompts via input(). Replace the shared reader thread with select()-based
non-blocking polling in _input_or_scan() — eliminates dangling threads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-19 19:13:56 +01:00
commit fd8de16bdd

View file

@ -5,11 +5,17 @@ from __future__ import annotations
import logging
import queue as _queue_module
import re
import select
import subprocess
import sys
import threading
from pathlib import Path
try:
import readline # noqa: F401 — aktiviert Zeileneditor für input()
except ImportError:
pass # Fallback: input() ohne Zeileneditor
from pydantic import BaseModel
from musiksammlung.cddb import get_discid, lookup_by_discid
@ -145,59 +151,40 @@ def _get_vision_result(
# ---------------------------------------------------------------------------
# Shared stdin reader — ein einziger Thread liest von stdin, alle Aufrufe
# von _read_line() und _input_or_scan() nutzen dieselbe Queue. Damit gibt
# es keine verwaisten Threads, die nach einem Foto-Upload weiterhin auf
# sys.stdin blockieren und nachfolgende Eingaben "stehlen".
# Zeileneditor für Benutzereingaben
#
# _read_line(prompt) — nutzt input() auf dem Hauptthread. Durch den Import
# von readline oben stehen Pfeiltasten, Backspace, Ctrl-A/E, History
# (Pfeil hoch/runter) usw. automatisch zur Verfügung.
#
# _input_or_scan(prompt, scanner) — pollt stdin UND Scanner-Queue parallel
# via select() ohne Background-Thread. Kein readline (nur terminal-
# eigenes Editing: Backspace, Ctrl-U), dafür keine verwaisten Threads.
# Betrifft nur EAN-Prompt und Disc-Insert — dort ist Komfort-Editing
# nicht nötig (Enter / kurze Ziffern).
# ---------------------------------------------------------------------------
_stdin_queue: _queue_module.Queue[str] = _queue_module.Queue()
_stdin_reader_lock = threading.Lock()
_stdin_reader_started = False
def _ensure_stdin_reader() -> None:
"""Startet den gemeinsamen stdin-Reader-Thread (einmalig, idempotent)."""
global _stdin_reader_started
with _stdin_reader_lock:
if _stdin_reader_started:
return
_stdin_reader_started = True
def _reader() -> None:
while True:
try:
line = sys.stdin.readline()
if not line: # EOF
_stdin_queue.put("")
break
_stdin_queue.put(line.rstrip("\n"))
except (EOFError, OSError):
_stdin_queue.put("")
break
threading.Thread(target=_reader, daemon=True).start()
def _read_line(prompt: str = "") -> str:
"""Liest eine Zeile von stdin über den Shared Reader.
"""Liest eine Zeile von stdin mit readline-Unterstützung.
Ersetzt input() überall in interactive_rip, damit keine konkurrierenden
Threads auf stdin warten.
Pfeiltasten, Backspace, Ctrl-A/E, History etc. funktionieren,
weil input() den GNU-readline-Editor nutzt (sofern importiert).
"""
_ensure_stdin_reader()
if prompt:
print(prompt, end="", flush=True)
return _stdin_queue.get()
try:
return input(prompt)
except EOFError:
return ""
def _input_or_scan(
prompt: str,
scanner: ScannerServer | None,
) -> tuple[str, Path | None]:
"""Kombiniertes stdin + Scanner-Queue: wartet gleichzeitig auf Tastatur und Foto.
"""Wartet gleichzeitig auf Tastatureingabe und Foto-Upload.
Nutzt den Shared stdin-Reader kein eigener Thread pro Aufruf.
Nutzt select() zum nicht-blockierenden Polling kein Background-Thread,
keine verwaisten Threads nach Foto-Upload.
Returns:
(eingegebener Text, None) wenn der User Enter drückt
@ -206,7 +193,6 @@ def _input_or_scan(
if scanner is None:
return _clean_input(_read_line(prompt)), None
_ensure_stdin_reader()
print(prompt, end="", flush=True)
while True:
@ -215,11 +201,10 @@ def _input_or_scan(
print("\n [Foto empfangen — weiter automatisch]", flush=True)
return "", photo
try:
val = _stdin_queue.get(timeout=0.1)
return _clean_input(val), None
except _queue_module.Empty:
continue
ready, _, _ = select.select([sys.stdin], [], [], 0.1)
if ready:
line = sys.stdin.readline().rstrip("\n")
return _clean_input(line), None