//! 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::fileio::Dateien; use crate::screen::TextScreen; use crate::value::Value; /// Breite einer Druckzone (Vorbild: 14 Zeichen). pub const ZONE_WIDTH: usize = 14; /// 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 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 { /// 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.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, 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, 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, screen: &mut TextScreen, dateien: &mut Dateien, n: i32, ) -> Result<(), RuntimeError> { let target = (n.max(1) as usize) - 1; if self.col(screen) > target { self.write(screen, dateien, "\n")?; } 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, screen: &mut TextScreen, dateien: &mut Dateien, n: i32, ) -> Result<(), RuntimeError> { if n > 0 { self.write(screen, dateien, &" ".repeat(n as usize))?; } Ok(()) } 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::*; /// 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, 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, 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, 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); } }