Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A
Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben). Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature. Vollstaendigkeits-Inventar - docs/inventar.md mit 285 Eintraegen aus den Themenlisten von bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle - crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten Bruchschritt (Puffer, Host, Korpus) - Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui behaelt Farbabbildung, Widget und den neuen Terminal-Host - Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus Ereignisse (Taste, Groessenaenderung, Abbruch) - Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht bestehenden Sollausgaben blieben dabei unveraendert Groessenunabhaengigkeit - 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor und VIEW PRINT geklemmt) - Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf Bibliothek - Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch) - Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS - Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON) - PRINT USING, LPRINT USING, FORMAT$, SetFormatCC - Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge) - Datum und Zeit mit Serienwerten ab 1899 - Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#, IRR#, MIRR#, SLN#, SYD#, DDB# - Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen), BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV - System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung Altlasten aus Phase 2 - ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar (prozeduruebergreifender Fixup im Codegenerator) - DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum) - Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest Vom Inventar aufgedeckte Fehler - Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die String*-Routinen, LINE und OPEN "COMn:") -- geschlossen - Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik, Record-Konvertierung, Rest der Dateisystemfunktionen - bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar bildet die Vereinigung mit qb45advr.hlp Neue Changes - phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb) - phase-3-ortszeit: zieht die UTC-Abweichung zurueck Neue Abhaengigkeit: unicode-width. Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei, docs/bibliothek.md neu, PLAN.md fortgeschrieben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,112 +1,199 @@
|
||||
//! Konsolen-Druckzustand: Spaltenverfolgung, PRINT-Formatierung,
|
||||
//! 14-Zeichen-Druckzonen, TAB/SPC. Wirkung ausschließlich über `Host`.
|
||||
//! Konsolen-Druckzustand: PRINT-Formatierung, 14-Zeichen-Druckzonen, TAB/SPC.
|
||||
//! Wirkung ausschließlich auf dem [`TextScreen`].
|
||||
//!
|
||||
//! Die Druckspalte wird nicht mehr eigenständig mitgezählt, sondern vom
|
||||
//! Bildschirm gelesen (`POS`). Damit stimmen Druckzonen und Cursor auch
|
||||
//! nach `LOCATE`, `CLS` oder einem Umbruch am rechten Rand überein.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::format::format_print;
|
||||
use crate::host::Host;
|
||||
use crate::fileio::Dateien;
|
||||
use crate::screen::TextScreen;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Breite einer Druckzone (Vorbild: 14 Zeichen).
|
||||
pub const ZONE_WIDTH: usize = 14;
|
||||
|
||||
/// Druckzustand (Spalte 0-basiert, in Zeichen).
|
||||
/// Wohin `PRINT` schreibt.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Ziel {
|
||||
#[default]
|
||||
Bildschirm,
|
||||
/// `PRINT #n` — in eine offene Datei.
|
||||
Datei(i32),
|
||||
/// `LPRINT` — in den Druckerkanal.
|
||||
Drucker,
|
||||
}
|
||||
|
||||
/// PRINT-Verhalten. Auf dem Bildschirm lebt die Spalte im Bildschirm; für
|
||||
/// Datei und Drucker wird sie hier mitgezählt, damit Druckzonen, `TAB` und
|
||||
/// `SPC` dort genauso wirken.
|
||||
#[derive(Default)]
|
||||
pub struct PrintState {
|
||||
pub col: usize,
|
||||
pub ziel: Ziel,
|
||||
/// Spalte (0-basiert) für Datei- und Druckerausgabe.
|
||||
pub spalte: usize,
|
||||
/// Druckerpuffer (Grundlage von `LPOS`); wird beim Programmende
|
||||
/// ausgegeben.
|
||||
pub drucker: String,
|
||||
}
|
||||
|
||||
impl PrintState {
|
||||
/// Text ausgeben und Spalte nachführen.
|
||||
pub fn write(&mut self, host: &mut dyn Host, s: &str) {
|
||||
host.write(s);
|
||||
/// Aktuelle Druckspalte (0-basiert) des eingestellten Ziels.
|
||||
pub fn col(&self, screen: &TextScreen) -> usize {
|
||||
match self.ziel {
|
||||
Ziel::Bildschirm => screen.pos() - 1,
|
||||
_ => self.spalte,
|
||||
}
|
||||
}
|
||||
|
||||
/// Text an das eingestellte Ziel ausgeben.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
s: &str,
|
||||
) -> Result<(), RuntimeError> {
|
||||
match self.ziel {
|
||||
Ziel::Bildschirm => screen.print(s),
|
||||
Ziel::Drucker => {
|
||||
self.drucker.push_str(s);
|
||||
self.spalte_nachfuehren(s);
|
||||
}
|
||||
Ziel::Datei(n) => {
|
||||
dateien.get(n)?.schreiben(s)?;
|
||||
self.spalte_nachfuehren(s);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spalte nach ausgegebenem Text nachführen (nur für Nicht-Bildschirm).
|
||||
fn spalte_nachfuehren(&mut self, s: &str) {
|
||||
match s.rfind('\n') {
|
||||
Some(i) => self.col = s[i + 1..].chars().count(),
|
||||
None => self.col += s.chars().count(),
|
||||
Some(i) => self.spalte = s[i + 1..].chars().count(),
|
||||
None => self.spalte += s.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
|
||||
/// Leerzeichen, Strings unverändert.
|
||||
pub fn print_value(&mut self, host: &mut dyn Host, v: &Value) {
|
||||
match v {
|
||||
Value::Str(s) => {
|
||||
let s = s.clone();
|
||||
self.write(host, &s);
|
||||
}
|
||||
_ => {
|
||||
let s = format_print(v);
|
||||
self.write(host, &s);
|
||||
}
|
||||
}
|
||||
pub fn print_value(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
v: &Value,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let text = match v {
|
||||
Value::Str(s) => s.to_string(),
|
||||
_ => format_print(v),
|
||||
};
|
||||
self.write(screen, dateien, &text)
|
||||
}
|
||||
|
||||
/// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
|
||||
pub fn print_comma(&mut self, host: &mut dyn Host) {
|
||||
let next = (self.col / ZONE_WIDTH + 1) * ZONE_WIDTH;
|
||||
let pad = next - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
pub fn print_comma(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let col = self.col(screen);
|
||||
let next = (col / ZONE_WIDTH + 1) * ZONE_WIDTH;
|
||||
self.write(screen, dateien, &" ".repeat(next - col))
|
||||
}
|
||||
|
||||
/// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
|
||||
/// dahinter, zuerst Zeilenumbruch (Vorbild).
|
||||
pub fn print_tab(&mut self, host: &mut dyn Host, n: i32) {
|
||||
pub fn print_tab(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
n: i32,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let target = (n.max(1) as usize) - 1;
|
||||
if self.col > target {
|
||||
self.write(host, "\n");
|
||||
if self.col(screen) > target {
|
||||
self.write(screen, dateien, "\n")?;
|
||||
}
|
||||
if target > self.col {
|
||||
let pad = target - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
let col = self.col(screen);
|
||||
if target > col {
|
||||
self.write(screen, dateien, &" ".repeat(target - col))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SPC(n)` — n Leerzeichen.
|
||||
pub fn print_spc(&mut self, host: &mut dyn Host, n: i32) {
|
||||
pub fn print_spc(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
n: i32,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if n > 0 {
|
||||
self.write(host, &" ".repeat(n as usize));
|
||||
self.write(screen, dateien, &" ".repeat(n as usize))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_newline(&mut self, host: &mut dyn Host) {
|
||||
self.write(host, "\n");
|
||||
pub fn print_newline(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
dateien: &mut Dateien,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.write(screen, dateien, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host::CaptureHost;
|
||||
|
||||
/// Zeile `z` (1-basiert) als getrimmter Text.
|
||||
fn zeile(s: &TextScreen, z: usize) -> String {
|
||||
let t: String = (1..=s.cols()).map(|c| s.cell(z, c).ch).collect();
|
||||
t.trim_end().to_string()
|
||||
}
|
||||
|
||||
fn ps() -> (PrintState, TextScreen, Dateien) {
|
||||
(PrintState::default(), TextScreen::new(), Dateien::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn druckzonen() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
// "a" (1 Zeichen) , → Spalte 14
|
||||
ps.write(&mut h, "a");
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "b");
|
||||
assert_eq!(h.output, "a b");
|
||||
assert_eq!(ps.col, 15);
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
// "a" (1 Zeichen) , → Spalte 15 (1-basiert)
|
||||
ps.write(&mut s, &mut d, "a").unwrap();
|
||||
ps.print_comma(&mut s, &mut d).unwrap();
|
||||
ps.write(&mut s, &mut d, "b").unwrap();
|
||||
assert_eq!(zeile(&s, 1), "a b");
|
||||
assert_eq!(s.pos(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volle_zone_springt_zur_uebernaechsten() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.write(&mut h, "12345678901234"); // 14 Zeichen, Zone voll
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "x");
|
||||
// x beginnt in Spalte 29 (1-basiert) = Index 28
|
||||
assert_eq!(h.output.chars().count(), 29);
|
||||
assert!(h.output.ends_with("x"));
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
ps.write(&mut s, &mut d, "12345678901234").unwrap(); // Zone voll
|
||||
ps.print_comma(&mut s, &mut d).unwrap();
|
||||
ps.write(&mut s, &mut d, "x").unwrap();
|
||||
// x beginnt in Spalte 29 (1-basiert)
|
||||
assert_eq!(s.cell(1, 29).ch, 'x');
|
||||
assert_eq!(s.pos(), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zahlen_mit_vorzeichenspalte() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.print_value(&mut h, &Value::Int(1));
|
||||
ps.print_value(&mut h, &Value::Int(-2));
|
||||
ps.print_newline(&mut h);
|
||||
assert_eq!(h.output, " 1 -2 \n");
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
ps.print_value(&mut s, &mut d, &Value::Int(1)).unwrap();
|
||||
ps.print_value(&mut s, &mut d, &Value::Int(-2)).unwrap();
|
||||
ps.print_newline(&mut s, &mut d).unwrap();
|
||||
assert_eq!(zeile(&s, 1), " 1 -2");
|
||||
assert_eq!(s.csrlin(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn druckzone_folgt_dem_cursor_nach_locate() {
|
||||
let (mut ps, mut s, mut d) = ps();
|
||||
s.locate(3, 5).unwrap();
|
||||
ps.print_comma(&mut s, &mut d).unwrap(); // Spalte 4 → nächste Zone 14
|
||||
assert_eq!(s.pos(), 15);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user