944 lines
40 KiB
Diff
944 lines
40 KiB
Diff
diff --git a/crates/tb-ide/src/render.rs b/crates/tb-ide/src/render.rs
|
||
index 9c64b24..1ab5a4b 100644
|
||
--- a/crates/tb-ide/src/render.rs
|
||
+++ b/crates/tb-ide/src/render.rs
|
||
@@ -11,74 +11,9 @@ use ratatui::{
|
||
};
|
||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||
|
||
-/// DOS-Farben der IDE. Die ersten 16 ANSI-Einträge gehören dem Terminalprofil.
|
||
-/// BASIC-Ausgabe verwendet weiterhin ihre eigene Farbabbildung in tb-ui.
|
||
+/// Feste DOS-RGB-Farben, identisch für IDE und BASIC-Ausgabe.
|
||
pub fn dos(n: u8) -> Color {
|
||
- static COLORS: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
|
||
- dos_color(
|
||
- n,
|
||
- *COLORS.get_or_init(|| {
|
||
- let detected = crossterm::style::available_color_count();
|
||
- // Ein generisches COLORTERM (z.B. "yes") verdeckt bei crossterm TERM.
|
||
- if detected < 256 && std::env::var("TERM").is_ok_and(|s| s.contains("256")) {
|
||
- 256
|
||
- } else {
|
||
- detected
|
||
- }
|
||
- }),
|
||
- )
|
||
-}
|
||
-
|
||
-pub fn dos_color(n: u8, colors: u16) -> Color {
|
||
- let n = n.min(15) as usize;
|
||
- const RGB: [(u8, u8, u8); 16] = [
|
||
- (0, 0, 0),
|
||
- (0, 0, 170),
|
||
- (0, 170, 0),
|
||
- (0, 170, 170),
|
||
- (170, 0, 0),
|
||
- (170, 0, 170),
|
||
- (170, 85, 0),
|
||
- (170, 170, 170),
|
||
- (85, 85, 85),
|
||
- (85, 85, 255),
|
||
- (85, 255, 85),
|
||
- (85, 255, 255),
|
||
- (255, 85, 85),
|
||
- (255, 85, 255),
|
||
- (255, 255, 85),
|
||
- (255, 255, 255),
|
||
- ];
|
||
- if colors > 256 {
|
||
- let (r, g, b) = RGB[n];
|
||
- Color::Rgb(r, g, b)
|
||
- } else if colors >= 256 {
|
||
- // Fester 6x6x6-Farbwürfel / Graurampe statt profilabhängiger Systemfarben.
|
||
- Color::Indexed(
|
||
- [
|
||
- 16, 19, 34, 37, 124, 127, 130, 248, 240, 63, 83, 87, 203, 207, 227, 231,
|
||
- ][n],
|
||
- )
|
||
- } else {
|
||
- [
|
||
- Color::Black,
|
||
- Color::Blue,
|
||
- Color::Green,
|
||
- Color::Cyan,
|
||
- Color::Red,
|
||
- Color::Magenta,
|
||
- Color::Yellow,
|
||
- Color::Gray,
|
||
- Color::DarkGray,
|
||
- Color::LightBlue,
|
||
- Color::LightGreen,
|
||
- Color::LightCyan,
|
||
- Color::LightRed,
|
||
- Color::LightMagenta,
|
||
- Color::LightYellow,
|
||
- Color::White,
|
||
- ][n]
|
||
- }
|
||
+ tb_ui::screen::basic_color(n.min(15))
|
||
}
|
||
|
||
pub(crate) fn pair(fg: u8, bg: u8) -> Style {
|
||
@@ -160,6 +95,8 @@ impl App {
|
||
5
|
||
},
|
||
);
|
||
+ // Block/Paragraph ändern Stile, löschen aber keine vorhandenen Zeichen.
|
||
+ f.render_widget(Clear, rect);
|
||
f.render_widget(
|
||
Block::default()
|
||
.borders(Borders::ALL)
|
||
diff --git a/crates/tb-ide/tests/app.rs b/crates/tb-ide/tests/app.rs
|
||
index 6ef2fdd..8ccd8c9 100644
|
||
--- a/crates/tb-ide/tests/app.rs
|
||
+++ b/crates/tb-ide/tests/app.rs
|
||
@@ -930,21 +930,7 @@ fn theme_rgb(color: ratatui::style::Color) -> (u8, u8, u8) {
|
||
use ratatui::style::Color;
|
||
match color {
|
||
Color::Rgb(r, g, b) => (r, g, b),
|
||
- Color::Indexed(n @ 16..=231) => {
|
||
- let n = n - 16;
|
||
- let channel = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
|
||
- (channel(n / 36), channel(n / 6 % 6), channel(n % 6))
|
||
- }
|
||
- Color::Indexed(n @ 232..=255) => {
|
||
- let v = 8 + (n - 232) * 10;
|
||
- (v, v, v)
|
||
- }
|
||
- _ => {
|
||
- let n = (0..16)
|
||
- .find(|n| tb_ide::render::dos_color(*n, 16) == color)
|
||
- .unwrap_or_else(|| panic!("Unbestimmte IDE-Farbe: {color:?}"));
|
||
- theme_rgb(tb_ide::render::dos_color(n, u16::MAX))
|
||
- }
|
||
+ _ => panic!("Keine absolute RGB-Farbe: {color:?}"),
|
||
}
|
||
}
|
||
fn theme_contrast(fg: ratatui::style::Color, bg: ratatui::style::Color) -> f64 {
|
||
@@ -983,23 +969,21 @@ fn assert_theme(app: &mut App, context: &str) {
|
||
|
||
#[test]
|
||
fn theme_contrast_covers_rendered_states_and_rejects_bad_pairs() {
|
||
- use tb_ide::render::dos_color;
|
||
- for colors in [256, u16::MAX] {
|
||
- for (fg, bg) in tb_ide::options::Options::default().colors {
|
||
- assert!(theme_contrast(dos_color(fg, colors), dos_color(bg, colors)) >= 4.5);
|
||
- }
|
||
- assert!(theme_contrast(dos_color(15, colors), dos_color(7, colors)) < 4.5);
|
||
- for n in 0..16 {
|
||
- let fg = if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
|
||
- 15
|
||
- } else {
|
||
- 0
|
||
- };
|
||
- assert!(
|
||
- theme_contrast(dos_color(fg, colors), dos_color(n, colors)) >= 4.5,
|
||
- "Palettenbeschriftung {n}"
|
||
- );
|
||
- }
|
||
+ use tb_ide::render::dos;
|
||
+ for (fg, bg) in tb_ide::options::Options::default().colors {
|
||
+ assert!(theme_contrast(dos(fg), dos(bg)) >= 4.5);
|
||
+ }
|
||
+ assert!(theme_contrast(dos(15), dos(7)) < 4.5);
|
||
+ for n in 0..16 {
|
||
+ let fg = if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
|
||
+ 15
|
||
+ } else {
|
||
+ 0
|
||
+ };
|
||
+ assert!(
|
||
+ theme_contrast(dos(fg), dos(n)) >= 4.5,
|
||
+ "Palettenbeschriftung {n}"
|
||
+ );
|
||
}
|
||
let t = Temp::new();
|
||
let mut app = t.app();
|
||
@@ -1086,34 +1070,23 @@ fn theme_color_commands_use_explicit_values_and_never_modify_terminal_palette()
|
||
buffer::Cell,
|
||
style::Color,
|
||
};
|
||
- use tb_ide::render::dos_color;
|
||
- assert_eq!(dos_color(1, u16::MAX), Color::Rgb(0, 0, 170));
|
||
- assert_eq!(dos_color(5, u16::MAX), Color::Rgb(170, 0, 170));
|
||
- assert_eq!(dos_color(6, u16::MAX), Color::Rgb(170, 85, 0));
|
||
- for colors in [16, 256, u16::MAX] {
|
||
- for n in 0..16 {
|
||
- let color = dos_color(n, colors);
|
||
- if let Color::Indexed(index) = color {
|
||
- assert!(index >= 16);
|
||
- }
|
||
- let mut cell = Cell::default();
|
||
- cell.set_symbol("X")
|
||
- .set_bg(color)
|
||
- .set_fg(dos_color(15, colors));
|
||
- let mut output = Vec::new();
|
||
- CrosstermBackend::new(&mut output)
|
||
- .draw(std::iter::once((0, 0, &cell)))
|
||
- .unwrap();
|
||
- let sequence = String::from_utf8(output).unwrap();
|
||
- assert!(sequence.contains('X'));
|
||
- if colors >= 256 {
|
||
- assert!(sequence.contains("48;"));
|
||
- }
|
||
- if colors > 256 {
|
||
- assert!(sequence.contains("48;2;"));
|
||
- }
|
||
- assert!(!sequence.contains("\x1b]")); // Keine globale Palettenänderung.
|
||
- }
|
||
+ use tb_ide::render::dos;
|
||
+ assert_eq!(dos(1), Color::Rgb(0, 0, 170));
|
||
+ assert_eq!(dos(5), Color::Rgb(170, 0, 170));
|
||
+ assert_eq!(dos(6), Color::Rgb(170, 85, 0));
|
||
+ for n in 0..16 {
|
||
+ assert_eq!(dos(n), tb_ui::screen::basic_color(n));
|
||
+ let mut cell = Cell::default();
|
||
+ cell.set_symbol("X").set_bg(dos(n)).set_fg(dos(15));
|
||
+ let mut output = Vec::new();
|
||
+ CrosstermBackend::new(&mut output)
|
||
+ .draw(std::iter::once((0, 0, &cell)))
|
||
+ .unwrap();
|
||
+ let sequence = String::from_utf8(output).unwrap();
|
||
+ assert!(sequence.contains('X'));
|
||
+ assert!(sequence.contains("38;2;255;255;255"));
|
||
+ assert!(sequence.contains("48;2;"));
|
||
+ assert!(!sequence.contains("\x1b]")); // Keine globale Palettenänderung.
|
||
}
|
||
}
|
||
|
||
@@ -1149,3 +1122,61 @@ fn theme_designer_chrome_and_handles_preserve_program_palette() {
|
||
assert_theme(&mut app, "Designer-Werkzeug");
|
||
}
|
||
}
|
||
+
|
||
+#[test]
|
||
+fn theme_windows_erase_desktop_and_underlying_windows_on_every_redraw() {
|
||
+ use ratatui::{
|
||
+ layout::Rect,
|
||
+ style::{Color, Modifier},
|
||
+ };
|
||
+ let t = Temp::new();
|
||
+ let mut app = t.app();
|
||
+ app.options.syntax_checking = false;
|
||
+ // Ein sichtbares Zeichen macht das Durchscheinen sicher erkennbar.
|
||
+ app.options.desktop = '@';
|
||
+ type_text(&mut app, "PRINT 42");
|
||
+ let mut term = Terminal::new(TestBackend::new(app.size.0, app.size.1)).unwrap();
|
||
+ for selected in [false, true, false] {
|
||
+ if selected {
|
||
+ key(&mut app, K::Char('a'), M::CONTROL);
|
||
+ } else {
|
||
+ plain(&mut app, K::Right);
|
||
+ }
|
||
+ term.draw(|f| app.render(f)).unwrap();
|
||
+ let b = term.backend().buffer();
|
||
+ let rect = app.rect(app.windows.iter().find(|w| w.id == app.active).unwrap());
|
||
+ let inner = Rect::new(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
|
||
+ for y in inner.y..inner.bottom() {
|
||
+ for x in inner.x..inner.right() {
|
||
+ let c = &b[(x, y)];
|
||
+ assert_ne!(c.symbol(), "@", "Desktop in Fenster bei {x}/{y}");
|
||
+ assert_eq!(c.bg, Color::Rgb(0, 0, 170));
|
||
+ if y > inner.y {
|
||
+ assert_eq!(c.symbol(), " ", "Restinhalt bei {x}/{y}");
|
||
+ assert!(!c.modifier.contains(Modifier::REVERSED));
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+ assert_eq!(b[(inner.x, inner.y)].symbol(), "P");
|
||
+ assert_eq!(
|
||
+ b[(inner.x, inner.y)].modifier.contains(Modifier::REVERSED),
|
||
+ selected
|
||
+ );
|
||
+ }
|
||
+ // Beim Aktivieren eines überlappenden Fensters müssen auch fremde Texte weg.
|
||
+ app.execute(Command::NewModule);
|
||
+ plain(&mut app, K::Enter);
|
||
+ assert!(app.dialog.is_none());
|
||
+ term.draw(|f| app.render(f)).unwrap();
|
||
+ let b = term.backend().buffer();
|
||
+ let rect = app.rect(app.windows.iter().find(|w| w.id == app.active).unwrap());
|
||
+ for y in rect.y + 1..rect.bottom() - 1 {
|
||
+ for x in rect.x + 1..rect.right() - 1 {
|
||
+ assert_eq!(
|
||
+ b[(x, y)].symbol(),
|
||
+ " ",
|
||
+ "Überdeckter Fensterinhalt bei {x}/{y}"
|
||
+ );
|
||
+ }
|
||
+ }
|
||
+}
|
||
diff --git a/crates/tb-ide/tests/execution.rs b/crates/tb-ide/tests/execution.rs
|
||
index 237ac71..e92a0b6 100644
|
||
--- a/crates/tb-ide/tests/execution.rs
|
||
+++ b/crates/tb-ide/tests/execution.rs
|
||
@@ -530,3 +530,35 @@ fn comment_only_source_change_also_requires_revision_choice() {
|
||
DialogKind::ResumeRevision
|
||
));
|
||
}
|
||
+
|
||
+#[test]
|
||
+fn platform_probe_records_each_basic_key_once_and_keeps_ide_keys_out() {
|
||
+ let t = Temp::new();
|
||
+ let log = t.0.join("platform-events.log");
|
||
+ let source = include_str!("../../../tests/platform/input.frm")
|
||
+ .replace("platform-events.log", &log.to_string_lossy());
|
||
+ let path = t.0.join("input.frm");
|
||
+ fs::write(&path, source).unwrap();
|
||
+ let mut a = t.app("");
|
||
+ a.load_initial_project(path).unwrap();
|
||
+ a.execute(Command::Start);
|
||
+ tick(&mut a);
|
||
+ key(&mut a, K::Char('a'), M::NONE);
|
||
+ tick(&mut a);
|
||
+ a.handle(Event::Key(KeyEvent::new_with_kind(
|
||
+ K::Char('a'),
|
||
+ M::NONE,
|
||
+ crossterm::event::KeyEventKind::Release,
|
||
+ )));
|
||
+ tick(&mut a);
|
||
+ let events = fs::read_to_string(&log).unwrap();
|
||
+ assert_eq!(
|
||
+ events.replace('\r', ""),
|
||
+ "Down: 97 : 0 \nPress: 97 : 0 \nUp: 97 : 0 \n"
|
||
+ );
|
||
+ edit_window(&mut a);
|
||
+ key(&mut a, K::F(1), M::NONE);
|
||
+ tick(&mut a);
|
||
+ assert_eq!(fs::read_to_string(&log).unwrap(), events);
|
||
+ assert!(matches!(a.active_window().unwrap().kind, WindowKind::Help));
|
||
+}
|
||
diff --git a/crates/tb-ui/examples/spike.rs b/crates/tb-ui/examples/spike.rs
|
||
index b6b5ff3..38196ed 100644
|
||
--- a/crates/tb-ui/examples/spike.rs
|
||
+++ b/crates/tb-ui/examples/spike.rs
|
||
@@ -1,15 +1,12 @@
|
||
//! Phase-0-Spike: 80×25-Bildschirmpuffer, 16-Farben-Palette, Unicode,
|
||
//! Tastatur- und Mausereignisse.
|
||
//!
|
||
-//! Start: `cargo run -p tb-ui --example spike` — Beenden mit Esc.
|
||
+//! Start: `cargo run -p tb-ui --features terminal --example spike -- --log events.txt` — Beenden mit Esc.
|
||
//! Getippte Zeichen erscheinen im Eingabebereich; Sondertasten und
|
||
//! Mausereignisse werden in der Statuszeile angezeigt.
|
||
|
||
-use crossterm::event::{
|
||
- self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
|
||
-};
|
||
-use crossterm::execute;
|
||
-use std::io::stdout;
|
||
+use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||
+use std::io::{stdout, Write};
|
||
use std::time::Duration;
|
||
use tb_ui::screen::{ScreenWidget, TextScreen};
|
||
|
||
@@ -57,8 +54,24 @@ fn status(s: &mut TextScreen, text: &str) {
|
||
}
|
||
|
||
fn main() -> anyhow::Result<()> {
|
||
- let mut terminal = ratatui::init();
|
||
- execute!(stdout(), EnableMouseCapture)?;
|
||
+ let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||
+ anyhow::ensure!(
|
||
+ args.is_empty() || (args.len() == 2 && args[0] == "--log"),
|
||
+ "Aufruf: spike [--log neue-datei]"
|
||
+ );
|
||
+ let mut log = if args.len() == 2 {
|
||
+ Some(
|
||
+ std::fs::OpenOptions::new()
|
||
+ .write(true)
|
||
+ .create_new(true)
|
||
+ .open(&args[1])?,
|
||
+ )
|
||
+ } else {
|
||
+ None
|
||
+ };
|
||
+ let _guard = tb_ui::terminal::TerminalGuard::enter(stdout())?;
|
||
+ let mut terminal = ratatui::Terminal::new(ratatui::backend::CrosstermBackend::new(stdout()))?;
|
||
+ let mut sequence = 0u64;
|
||
|
||
let (tw, th) = crossterm::terminal::size()?;
|
||
let mut screen = TextScreen::with_size(tw as usize, th as usize);
|
||
@@ -70,7 +83,12 @@ fn main() -> anyhow::Result<()> {
|
||
if !event::poll(Duration::from_millis(100))? {
|
||
continue;
|
||
}
|
||
- match event::read()? {
|
||
+ let received = event::read()?;
|
||
+ sequence += 1;
|
||
+ if let Some(log) = &mut log {
|
||
+ writeln!(log, "{sequence}: {received:?}")?;
|
||
+ }
|
||
+ match received {
|
||
Event::Key(k) if k.kind != KeyEventKind::Release => match k.code {
|
||
KeyCode::Esc => break,
|
||
KeyCode::Char(c) => {
|
||
@@ -105,7 +123,5 @@ fn main() -> anyhow::Result<()> {
|
||
}
|
||
}
|
||
|
||
- execute!(stdout(), DisableMouseCapture)?;
|
||
- ratatui::restore();
|
||
Ok(())
|
||
}
|
||
diff --git a/crates/tb-ui/src/screen.rs b/crates/tb-ui/src/screen.rs
|
||
index 8727b91..8297812 100644
|
||
--- a/crates/tb-ui/src/screen.rs
|
||
+++ b/crates/tb-ui/src/screen.rs
|
||
@@ -12,11 +12,28 @@ use ratatui::layout::Rect;
|
||
use ratatui::style::{Color, Style};
|
||
use ratatui::widgets::Widget;
|
||
|
||
-/// Abbildung der klassischen 16-Farben-Palette auf ANSI-Indexfarben.
|
||
-/// (Klassisch: 1 = Blau, 4 = Rot — ANSI: 1 = Rot, 4 = Blau usw.)
|
||
+/// Klassische DOS-Farbnummern als absolute RGB-Werte, ohne Terminalprofil.
|
||
pub fn basic_color(n: u8) -> Color {
|
||
- const MAP: [u8; 16] = [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15];
|
||
- Color::Indexed(MAP[(n & 0x0F) as usize])
|
||
+ const RGB: [(u8, u8, u8); 16] = [
|
||
+ (0, 0, 0),
|
||
+ (0, 0, 170),
|
||
+ (0, 170, 0),
|
||
+ (0, 170, 170),
|
||
+ (170, 0, 0),
|
||
+ (170, 0, 170),
|
||
+ (170, 85, 0),
|
||
+ (170, 170, 170),
|
||
+ (85, 85, 85),
|
||
+ (85, 85, 255),
|
||
+ (85, 255, 85),
|
||
+ (85, 255, 255),
|
||
+ (255, 85, 85),
|
||
+ (255, 85, 255),
|
||
+ (255, 255, 85),
|
||
+ (255, 255, 255),
|
||
+ ];
|
||
+ let (r, g, b) = RGB[(n & 0x0F) as usize];
|
||
+ Color::Rgb(r, g, b)
|
||
}
|
||
|
||
/// Zeichenbares Gegenstück zu [`TextScreen`].
|
||
@@ -66,9 +83,9 @@ mod tests {
|
||
|
||
#[test]
|
||
fn farbabbildung() {
|
||
- assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau
|
||
- assert_eq!(basic_color(4), Color::Indexed(1)); // klassisch Rot
|
||
- assert_eq!(basic_color(14), Color::Indexed(11)); // Gelb
|
||
- assert_eq!(basic_color(15), Color::Indexed(15)); // Weiß
|
||
+ assert_eq!(basic_color(1), Color::Rgb(0, 0, 170)); // klassisch Blau
|
||
+ assert_eq!(basic_color(4), Color::Rgb(170, 0, 0)); // klassisch Rot
|
||
+ assert_eq!(basic_color(14), Color::Rgb(255, 255, 85)); // Gelb
|
||
+ assert_eq!(basic_color(15), Color::Rgb(255, 255, 255)); // Weiß
|
||
}
|
||
}
|
||
diff --git a/crates/tb-ui/src/terminal.rs b/crates/tb-ui/src/terminal.rs
|
||
index 77493b2..67a6c8d 100644
|
||
--- a/crates/tb-ui/src/terminal.rs
|
||
+++ b/crates/tb-ui/src/terminal.rs
|
||
@@ -11,23 +11,47 @@ use std::io::{self, Write};
|
||
pub struct TerminalGuard<W: Write> {
|
||
writer: W,
|
||
raw: fn(bool) -> io::Result<()>,
|
||
+ #[cfg(windows)]
|
||
+ modes: Option<ConsoleModes>,
|
||
}
|
||
impl<W: Write> TerminalGuard<W> {
|
||
pub fn enter(writer: W) -> io::Result<Self> {
|
||
- Self::with_raw(writer, |on| {
|
||
+ // Farben tragen UI-/BASIC-Zustand und müssen auch ohne Farberkennung
|
||
+ // oder mit NO_COLOR als explizite RGB-Werte ausgegeben werden.
|
||
+ crossterm::style::force_color_output(true);
|
||
+ #[cfg(windows)]
|
||
+ let modes = ConsoleModes::capture()?;
|
||
+ #[cfg(windows)]
|
||
+ modes.enable_output()?;
|
||
+ #[allow(unused_mut)]
|
||
+ let mut guard = Self::with_raw(writer, |on| {
|
||
if on {
|
||
enable_raw_mode()
|
||
} else {
|
||
disable_raw_mode()
|
||
}
|
||
- })
|
||
+ })?;
|
||
+ #[cfg(windows)]
|
||
+ {
|
||
+ guard.modes = Some(modes);
|
||
+ }
|
||
+ Ok(guard)
|
||
}
|
||
fn with_raw(writer: W, raw: fn(bool) -> io::Result<()>) -> io::Result<Self> {
|
||
- let mut guard = Self { writer, raw };
|
||
+ let mut guard = Self {
|
||
+ writer,
|
||
+ raw,
|
||
+ #[cfg(windows)]
|
||
+ modes: None,
|
||
+ };
|
||
guard.resume()?;
|
||
Ok(guard)
|
||
}
|
||
fn resume(&mut self) -> io::Result<()> {
|
||
+ #[cfg(windows)]
|
||
+ if let Some(modes) = &self.modes {
|
||
+ modes.enable_output()?;
|
||
+ }
|
||
(self.raw)(true)?;
|
||
execute!(
|
||
self.writer,
|
||
@@ -38,15 +62,18 @@ impl<W: Write> TerminalGuard<W> {
|
||
)
|
||
}
|
||
fn suspend(&mut self) -> io::Result<()> {
|
||
- (self.raw)(false)?;
|
||
- execute!(
|
||
+ let raw = (self.raw)(false);
|
||
+ let output = execute!(
|
||
self.writer,
|
||
ResetColor,
|
||
Show,
|
||
DisableMouseCapture,
|
||
DisableBracketedPaste,
|
||
LeaveAlternateScreen
|
||
- )
|
||
+ );
|
||
+ #[cfg(windows)]
|
||
+ let output = output.and(self.modes.as_ref().map_or(Ok(()), ConsoleModes::restore));
|
||
+ raw.and(output)
|
||
}
|
||
/// Foreground child owns cooked terminal input. Always restore the IDE,
|
||
/// including spawn/wait errors and a child interrupted by Ctrl+C.
|
||
@@ -91,6 +118,10 @@ impl<W: Write> TerminalGuard<W> {
|
||
}
|
||
impl<W: Write> Drop for TerminalGuard<W> {
|
||
fn drop(&mut self) {
|
||
+ #[cfg(windows)]
|
||
+ if let Some(modes) = &self.modes {
|
||
+ let _ = modes.enable_output();
|
||
+ }
|
||
let _ = (self.raw)(false);
|
||
let _ = execute!(
|
||
self.writer,
|
||
@@ -103,6 +134,58 @@ impl<W: Write> Drop for TerminalGuard<W> {
|
||
}
|
||
}
|
||
|
||
+// Crossterm stellt unter Windows Raw-/Mausflags pauschal zurück. Der Besitzer
|
||
+// muss zusätzlich die tatsächlichen Eingabe- und Ausgabemodi wiederherstellen.
|
||
+#[cfg(windows)]
|
||
+#[derive(Debug)]
|
||
+struct ConsoleModes([(std::os::windows::io::RawHandle, u32); 2]);
|
||
+#[cfg(windows)]
|
||
+#[link(name = "kernel32")]
|
||
+extern "system" {
|
||
+ fn GetStdHandle(kind: u32) -> std::os::windows::io::RawHandle;
|
||
+ fn GetConsoleMode(handle: std::os::windows::io::RawHandle, mode: *mut u32) -> i32;
|
||
+ fn SetConsoleMode(handle: std::os::windows::io::RawHandle, mode: u32) -> i32;
|
||
+}
|
||
+#[cfg(windows)]
|
||
+impl ConsoleModes {
|
||
+ fn capture() -> io::Result<Self> {
|
||
+ let mut modes = [(std::ptr::null_mut(), 0); 2];
|
||
+ for (slot, kind) in modes.iter_mut().zip([(-10i32) as u32, (-11i32) as u32]) {
|
||
+ // Borrowed standard handles: valid until the terminal owner exits.
|
||
+ slot.0 = unsafe { GetStdHandle(kind) };
|
||
+ if unsafe { GetConsoleMode(slot.0, &mut slot.1) } == 0 {
|
||
+ return Err(io::Error::last_os_error());
|
||
+ }
|
||
+ }
|
||
+ Ok(Self(modes))
|
||
+ }
|
||
+ fn enable_output(&self) -> io::Result<()> {
|
||
+ // Crossterm caches ANSI support, but a Shell handoff restores the old
|
||
+ // output flags. Re-enable VT for each ownership cycle, including re-entry.
|
||
+ if crossterm::ansi_support::supports_ansi()
|
||
+ && unsafe { SetConsoleMode(self.0[1].0, self.0[1].1 | 0x0004) } == 0
|
||
+ {
|
||
+ return Err(io::Error::last_os_error());
|
||
+ }
|
||
+ Ok(())
|
||
+ }
|
||
+ fn restore(&self) -> io::Result<()> {
|
||
+ let mut result = Ok(());
|
||
+ for (handle, mode) in self.0 {
|
||
+ if unsafe { SetConsoleMode(handle, mode) } == 0 && result.is_ok() {
|
||
+ result = Err(io::Error::last_os_error());
|
||
+ }
|
||
+ }
|
||
+ result
|
||
+ }
|
||
+}
|
||
+#[cfg(windows)]
|
||
+impl Drop for ConsoleModes {
|
||
+ fn drop(&mut self) {
|
||
+ let _ = self.restore();
|
||
+ }
|
||
+}
|
||
+
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
@@ -149,7 +232,7 @@ mod tests {
|
||
("exit 7", false, 7),
|
||
(
|
||
if cfg!(windows) {
|
||
- "ping -n 30 127.0.0.1 >nul"
|
||
+ "for /L %i in (1,1,1000000000) do @rem"
|
||
} else {
|
||
"exec sleep 30"
|
||
},
|
||
@@ -160,10 +243,13 @@ mod tests {
|
||
let out = Output(Rc::default());
|
||
let copy = out.clone();
|
||
let mut guard = TerminalGuard::with_raw(out, raw).unwrap();
|
||
- assert_eq!(
|
||
- guard.shell_with_interrupt(command, || abort).unwrap(),
|
||
- expected
|
||
- );
|
||
+ let started = std::time::Instant::now();
|
||
+ let code = guard.shell_with_interrupt(command, || abort).unwrap();
|
||
+ if !abort {
|
||
+ assert_eq!(code, expected);
|
||
+ }
|
||
+ // Windows TerminateProcess liefert einen Code; Unix ein Signal.
|
||
+ assert!(started.elapsed() < std::time::Duration::from_secs(5));
|
||
assert_eq!(RAW.load(Ordering::SeqCst), 1);
|
||
let text = String::from_utf8(copy.0.borrow().clone()).unwrap();
|
||
assert_eq!(text.matches("?1049h").count(), 2);
|
||
@@ -187,4 +273,111 @@ mod tests {
|
||
.unwrap()
|
||
.contains("?25h"));
|
||
}
|
||
+ /// Run explicitly on Windows; never counts as a Windows Terminal UI test.
|
||
+ #[cfg(windows)]
|
||
+ #[test]
|
||
+ #[ignore = "benötigt Windows mit echter Konsole; isolierter Kindprozess"]
|
||
+ fn windows_console_restores_modes() {
|
||
+ use std::os::windows::{io::AsRawHandle, process::CommandExt};
|
||
+ use std::process::{Command, Stdio};
|
||
+ use std::time::{Duration, Instant};
|
||
+ const CHILD: &str = "TB_CONSOLE_TEST_CHILD";
|
||
+ if std::env::var_os(CHILD).is_none() {
|
||
+ let mut child = Command::new(std::env::current_exe().unwrap())
|
||
+ .args([
|
||
+ "--exact",
|
||
+ "terminal::tests::windows_console_restores_modes",
|
||
+ "--ignored",
|
||
+ "--nocapture",
|
||
+ ])
|
||
+ .env(CHILD, "1")
|
||
+ .creation_flags(0x00000010) // CREATE_NEW_CONSOLE: never touches the caller's console.
|
||
+ .stdin(Stdio::null())
|
||
+ .stdout(Stdio::null())
|
||
+ .stderr(Stdio::null())
|
||
+ .spawn()
|
||
+ .unwrap();
|
||
+ let deadline = Instant::now() + Duration::from_secs(30);
|
||
+ loop {
|
||
+ if let Some(status) = child.try_wait().unwrap() {
|
||
+ assert!(
|
||
+ status.success(),
|
||
+ "Isolierte Windows-Konsolenprüfung: {status}"
|
||
+ );
|
||
+ return;
|
||
+ }
|
||
+ if Instant::now() >= deadline {
|
||
+ let _ = child.kill();
|
||
+ let _ = child.wait();
|
||
+ panic!("Windows-Konsolenprüfung hat die Frist überschritten");
|
||
+ }
|
||
+ std::thread::sleep(Duration::from_millis(20));
|
||
+ }
|
||
+ }
|
||
+ #[link(name = "kernel32")]
|
||
+ extern "system" {
|
||
+ fn SetStdHandle(kind: u32, handle: std::os::windows::io::RawHandle) -> i32;
|
||
+ }
|
||
+ let open = |name| {
|
||
+ std::fs::OpenOptions::new()
|
||
+ .read(true)
|
||
+ .write(true)
|
||
+ .open(name)
|
||
+ .unwrap()
|
||
+ };
|
||
+ let input = open("CONIN$");
|
||
+ let output = open("CONOUT$");
|
||
+ // Standard streams started as NUL; bind to this child's own new console.
|
||
+ assert_ne!(
|
||
+ unsafe { SetStdHandle((-10i32) as u32, input.as_raw_handle()) },
|
||
+ 0
|
||
+ );
|
||
+ assert_ne!(
|
||
+ unsafe { SetStdHandle((-11i32) as u32, output.as_raw_handle()) },
|
||
+ 0
|
||
+ );
|
||
+ let original = ConsoleModes::capture().unwrap();
|
||
+ // Non-default echo flags catch a reset-to-default masquerading as restore.
|
||
+ let mode = original.0[0].1 & !0x0004; // ENABLE_ECHO_INPUT
|
||
+ assert_ne!(unsafe { SetConsoleMode(input.as_raw_handle(), mode) }, 0);
|
||
+ let expected = ConsoleModes::capture().unwrap();
|
||
+ let assert_restored = || assert_eq!(ConsoleModes::capture().unwrap().0, expected.0);
|
||
+ for (command, abort, code) in [
|
||
+ ("exit 0", false, 0),
|
||
+ ("exit 7", false, 7),
|
||
+ ("for /L %i in (1,1,1000000000) do @rem", true, 0),
|
||
+ ] {
|
||
+ let mut guard = TerminalGuard::enter(io::stdout()).unwrap();
|
||
+ guard.suspend().unwrap();
|
||
+ assert_restored();
|
||
+ guard.resume().unwrap();
|
||
+ if crossterm::ansi_support::supports_ansi() {
|
||
+ assert_ne!(ConsoleModes::capture().unwrap().0[1].1 & 0x0004, 0);
|
||
+ }
|
||
+ let result = guard.shell_with_interrupt(command, || abort).unwrap();
|
||
+ if !abort {
|
||
+ assert_eq!(result, code);
|
||
+ }
|
||
+ assert!(crossterm::terminal::is_raw_mode_enabled().unwrap());
|
||
+ drop(guard);
|
||
+ assert_restored();
|
||
+ }
|
||
+ let error = (|| -> io::Result<()> {
|
||
+ let _guard = TerminalGuard::enter(io::stdout())?;
|
||
+ Err(io::Error::other("Erzwungener Renderfehler"))
|
||
+ })();
|
||
+ assert!(error.is_err());
|
||
+ assert_restored();
|
||
+ struct Broken;
|
||
+ impl Write for Broken {
|
||
+ fn write(&mut self, _: &[u8]) -> io::Result<usize> {
|
||
+ Err(io::Error::other("Initialisierung"))
|
||
+ }
|
||
+ fn flush(&mut self) -> io::Result<()> {
|
||
+ Ok(())
|
||
+ }
|
||
+ }
|
||
+ assert!(TerminalGuard::enter(Broken).is_err());
|
||
+ assert_restored();
|
||
+ }
|
||
}
|
||
diff --git a/docs/ide-referenz.md b/docs/ide-referenz.md
|
||
index 6cfa05d..6104316 100644
|
||
--- a/docs/ide-referenz.md
|
||
+++ b/docs/ide-referenz.md
|
||
@@ -102,24 +102,23 @@ Laufzeit-Defaults kompilierter Programme weichen ab (Titel Weiß auf Schwarz).
|
||
|
||
## 4a. Farbabbildung und Kontrast (Phase 6)
|
||
|
||
-Die DOS-Farbnummern in Options→Display bleiben erhalten. Die IDE verwendet
|
||
-bei `COLORTERM=truecolor`/`24bit` explizite RGB-Werte der klassischen
|
||
-16-Farben-Palette; bei `TERM=…256color` feste Einträge im Farbwürfel und der
|
||
-Graurampe außerhalb der ersten 16 ANSI-Farben. Ein generisches `COLORTERM`
|
||
-verdeckt diese 256-Farben-Erkennung nicht. Auf Windows verwendet die IDE
|
||
-zusätzlich die ANSI-Fähigkeitserkennung des vorhandenen Terminaladapters.
|
||
-Die Erkennung erfolgt einmal pro Prozess; nach Profiländerungen neu starten.
|
||
-
|
||
-Damit hängt das Standardtheme bei expliziter Farbausgabe nicht von einer
|
||
-pastellfarbenen ANSI-Palette ab. Beispielwerte: Codehintergrund `#0000AA`,
|
||
-Codeschrift `#AAAAAA`, aktiver Titel `#FFFFFF` auf `#AA00AA`. DOS-Farbe 6
|
||
-ist Braun (`#AA5500`), nicht das frei konfigurierte ANSI-Gelb. Der
|
||
-256-Farben-Fallback nähert diese Werte an (`#0000AF`, `#A8A8A8`, `#AF00AF`).
|
||
-Die globale Terminalpalette wird nicht verändert. Unbekannte beziehungsweise
|
||
-reine 16-Farb-Terminals verwenden weiterhin die ANSI-Zuordnung; dort ist ein
|
||
-geeignetes Terminalprofil nötig. `NO_COLOR`, Transparenz, Farbfilter und
|
||
-Inaktivitätsdimmung des Emulators können die Darstellung zusätzlich verändern;
|
||
-die IDE überschreibt solche Benutzereinstellungen nicht.
|
||
+Die DOS-Farbnummern in Options→Display bleiben erhalten. IDE, Formularvorschau
|
||
+und BASIC-Ausgabe verwenden dieselbe feste klassische RGB-Palette. Jede
|
||
+Farbe wird als 24-Bit-Vorder-/Hintergrundwert ausgegeben, unabhängig von
|
||
+`TERM`, `COLORTERM`, `NO_COLOR` oder der ANSI-Palette des Terminalprofils.
|
||
+Es gibt keinen Rückfall auf Index- oder Profilfarben. Die globale
|
||
+Terminalpalette wird nicht verändert; gespeichert werden weiterhin
|
||
+DOS-Farbnummern, und BASIC bestimmt seine eigenen ForeColor/BackColor-Werte.
|
||
+
|
||
+Beispielwerte: Codehintergrund `#0000AA`, Codeschrift `#AAAAAA`, aktiver
|
||
+Titel `#FFFFFF` auf `#AA00AA`. DOS-Farbe 6 ist Braun (`#AA5500`).
|
||
+Fensterflächen werden vor dem Zeichnen geleert, damit Desktop-Füllzeichen
|
||
+und Inhalte verdeckter Fenster keine scheinbar helleren Hintergründe erzeugen.
|
||
+
|
||
+Die Ausgabe setzt einen Terminalemulator mit 24-Bit-Farbunterstützung voraus.
|
||
+Fehlende RGB-Unterstützung bleibt ein offener Zielbefund. Transparenz,
|
||
+Farbfilter und Inaktivitätsdimmung sind nachgelagerte Emulatorfunktionen;
|
||
+sie werden für die visuelle Farbprüfung getrennt protokolliert.
|
||
|
||
### Referenzfundstellen
|
||
|
||
@@ -144,7 +143,7 @@ Palette. Ansichten belegen Flächen und Rollen, keine heutige Terminalausgabe.
|
||
| Deaktivierte Beschriftungen | Nicht ausreichend durch diese Ansichten belegt | Lesbares Schwarz auf Grau, Menüs zusätzlich `×` und kursiv |
|
||
| Hilfe und Debuggermarkierungen | In diesen Ansichten nicht belegt | Dunkelblaue Hilfetitel/Links auf Grau; Fokus Weiß auf Blau; Breakpoints Weiß auf Rot, Ausführungszeile Schwarz auf Gelb |
|
||
|
||
-Die Standardpaare müssen bei RGB- und 256-Farben-Ausgabe mindestens 4,5:1
|
||
+Die Standardpaare müssen bei RGB-Ausgabe mindestens 4,5:1
|
||
Textkontrast erreichen; notwendige nichttextuelle Fokusmarkierungen mindestens
|
||
3:1. Die Zelltests prüfen auch Rahmen und Handles auf mindestens 4,5:1
|
||
gegen ihren eigenen Hintergrund. Benutzerauswahl in Display bleibt frei;
|
||
@@ -154,16 +153,16 @@ Designer-Werkzeuge und Handles die IDE-Farben verwenden.
|
||
|
||
Die sRGB-Kontrastberechnung ergibt für die wichtigsten Standardpaare:
|
||
|
||
-| Rolle | RGB | 256 Farben |
|
||
-|---|---|---|
|
||
-| Code / Hilfelink | 5,72:1 | 5,45:1 |
|
||
-| Aktiver Titel | 6,38:1 | 6,10:1 |
|
||
-| Menü / Dialog / deaktivierter Text | 9,04:1 | 8,83:1 |
|
||
-| Status / Werkzeuge | 7,33:1 | 7,75:1 |
|
||
-| Auswahl | 21,00:1 | 21,00:1 |
|
||
-| Linkfokus / Debugger | 13,29:1 | 12,97:1 |
|
||
-| Breakpoint | 7,75:1 | 7,44:1 |
|
||
-| Ausführung / Handle | 19,69:1 | 19,72:1 |
|
||
+| Rolle | RGB-Kontrast |
|
||
+|---|---|
|
||
+| Code / Hilfelink | 5,72:1 |
|
||
+| Aktiver Titel | 6,38:1 |
|
||
+| Menü / Dialog / deaktivierter Text | 9,04:1 |
|
||
+| Status / Werkzeuge | 7,33:1 |
|
||
+| Auswahl | 21,00:1 |
|
||
+| Linkfokus / Debugger | 13,29:1 |
|
||
+| Breakpoint | 7,75:1 |
|
||
+| Ausführung / Handle | 19,69:1 |
|
||
|
||
### Prüffolge für Change 05
|
||
|
||
@@ -188,10 +187,10 @@ Linux amd64/xterm, Linux amd64/VTE, Linux arm64/xterm und Linux arm64/VTE:
|
||
Farbbeschriftungen und Handles; ForeColor/BackColor der Vorschau bleiben
|
||
unabhängig vom IDE-Theme.
|
||
6. Dieselben Ansichten mit absichtlich abweichender ANSI-Palette prüfen.
|
||
- RGB-/256-Farben der IDE müssen stabil bleiben. Transparenz und
|
||
+ RGB-Farben von IDE und BASIC-Ausgabe müssen stabil bleiben. Transparenz und
|
||
Inaktivitätsdimmung getrennt ein-/ausschalten und Abweichungen als
|
||
- Emulatorverhalten dokumentieren. Für 16-Farb-Fallback das tatsächlich
|
||
- geeignete Profil und verbleibende Unterschiede festhalten.
|
||
+ Emulatorverhalten dokumentieren. Auch ohne `COLORTERM`, mit `TERM=vt100`
|
||
+ und mit `NO_COLOR=1` muss die Anwendung dieselben RGB-Werte ausgeben.
|
||
7. Options ändern, speichern und neu starten; Auswahl muss erhalten sein.
|
||
IDE beenden: globale Palette und normale Shell-Anzeige müssen erhalten sein.
|
||
|
||
diff --git a/openspec/changes/phase-6-05-plattformmatrix/design.md b/openspec/changes/phase-6-05-plattformmatrix/design.md
|
||
index 22aa012..6e3fa4d 100644
|
||
--- a/openspec/changes/phase-6-05-plattformmatrix/design.md
|
||
+++ b/openspec/changes/phase-6-05-plattformmatrix/design.md
|
||
@@ -19,6 +19,8 @@ Phase 5 verwendete crossterm-Ereignisse, ratatui-TestBackend und `tests/support/
|
||
|
||
7. Den ergänzenden Theme-Change 05a vor Abschluss der visuellen Matrix berücksichtigen. Dessen Referenzansichten und Kontrastprüfungen mit tatsächlicher Revision, Farbprofil und Standard-/abweichender ANSI-Palette in jeder Pflichtzelle prüfen; vor einer Theme-Korrektur erhobene betroffene Bilder erneut aufnehmen. Diese Nachweise gehen mit der bestehenden Matrix an 06/07.
|
||
|
||
+8. Benutzerbefund vom 07.09.2026: ANSI-/256-Fallback entfernen und die vorhandene Farbabbildung in tb-ui für IDE und BASIC gemeinsam nutzen. RGB-Ausgabe auch bei fehlenden Farbfähigkeitsvariablen und NO_COLOR erzwingen. Fenster vor Block/Paragraph leeren, da diese nur Stile setzen und sonst Desktopmuster oder verdeckte Texte durchscheinen. Regressionen prüfen tatsächliche Leerzeichen, Auswahl/Neuzeichnen, absolute Farbwerte und ausgegebene Escape-Sequenzen. Frühere visuelle Nachweise gelten nicht für diesen korrigierten Stand.
|
||
+
|
||
## Risks / Trade-offs
|
||
|
||
- CI ohne interaktiven Desktop → automatisierte Zieltests plus zugeordnete manuelle Emulatornachweise, keine Gleichsetzung beider Kategorien.
|
||
diff --git a/openspec/changes/phase-6-05-plattformmatrix/proposal.md b/openspec/changes/phase-6-05-plattformmatrix/proposal.md
|
||
index 0e2efa2..ca383fa 100644
|
||
--- a/openspec/changes/phase-6-05-plattformmatrix/proposal.md
|
||
+++ b/openspec/changes/phase-6-05-plattformmatrix/proposal.md
|
||
@@ -18,7 +18,8 @@ Die bisherige headless Abnahme und der lokale Unix-PTY-Test beweisen nicht die e
|
||
|
||
### Modified Capabilities
|
||
|
||
-Keine. Bestehende IDE- und Host-Eingabeverträge bleiben maßgeblich.
|
||
+- `ide-oberflaeche`: Durchgehend absolute RGB-Farben und vollständig geleerte Fensterflächen; Benutzerkorrektur vom 07.09.2026.
|
||
+- `textbildschirm`: Dieselbe absolute DOS-RGB-Palette für programmbestimmte BASIC-Farben und Formularvorschau.
|
||
|
||
## Impact
|
||
|
||
diff --git a/openspec/changes/phase-6-05-plattformmatrix/tasks.md b/openspec/changes/phase-6-05-plattformmatrix/tasks.md
|
||
index da2b71e..5ab64d7 100644
|
||
--- a/openspec/changes/phase-6-05-plattformmatrix/tasks.md
|
||
+++ b/openspec/changes/phase-6-05-plattformmatrix/tasks.md
|
||
@@ -1,8 +1,8 @@
|
||
## 1. Prüfpfade vorbereiten
|
||
|
||
-- [ ] 1.1 Verbindliche Matrix und kurze Bediensequenzen für die vier Targets anlegen; Windows Terminal, Terminal.app sowie xterm/VTE auf beiden Linux-Architekturen müssen konkrete Versions-/Revisionsfelder und erwartete Resultate besitzen.
|
||
+- [x] 1.1 Verbindliche Matrix und kurze Bediensequenzen für die vier Targets anlegen; Windows Terminal, Terminal.app sowie xterm/VTE auf beiden Linux-Architekturen müssen konkrete Versions-/Revisionsfelder und erwartete Resultate besitzen.
|
||
- [ ] 1.2 Bestehenden Unix-PTY-Test und passenden Windows-Konsolen-/ConPTY-Test für Shell, Abbruch und Wiederherstellung bereitstellen; positive Läufe und erzwungene Fehler dürfen keinen beschädigten Terminalmodus hinterlassen.
|
||
-- [ ] 1.3 Die gemeinsame TBL-Verbraucherprobe aus 03 samt fester Prüfsumme in jede der vier Zielabnahmen aufnehmen (quellfreies TBC/EXE, RUN, tatsächliches Host-Target); IDE-/Programmprüfungen für F1–F12, Modifikatoren, Maus, Unicode/Attribute und Resize nachvollziehbar auflisten; jeder Fall muss sichtbaren Effekt und Ausschluss doppelter BASIC-Zustellung prüfen.
|
||
+- [x] 1.3 Die gemeinsame TBL-Verbraucherprobe aus 03 samt fester Prüfsumme in jede der vier Zielabnahmen aufnehmen (quellfreies TBC/EXE, RUN, tatsächliches Host-Target); IDE-/Programmprüfungen für F1–F12, Modifikatoren, Maus, Unicode/Attribute und Resize nachvollziehbar auflisten; jeder Fall muss sichtbaren Effekt und Ausschluss doppelter BASIC-Zustellung prüfen.
|
||
|
||
## 2. Tatsächliche Zielabnahme
|
||
|
||
@@ -16,3 +16,11 @@
|
||
|
||
- [ ] 3.1 Terminalgrenzen und verifizierte Ersatzwege dokumentieren; kein Produktfehler oder fehlender Prüfrechner darf als bestandene Matrixzelle erscheinen.
|
||
- [ ] 3.2 Automatisierbare Zielprüfungen an 06 übergeben und Matrixbericht fertigstellen; alle Pflichtzellen, Revisionen, vier Ergebnisse derselben TBL-Verbraucherprobe und tatsächlichen Ergebnisse müssen vor Abschluss vorliegen. Die weiterhin offenen Archivaufgaben 05a/1.2 (konkretes Benutzerprofil, frische/gespeicherte Optionen, Transparenz/Dimmung), 05a/3.2 (visuelle Matrix) und 05a/3.3 (befundfreie Schlussverifizierung) müssen hier tatsächlich abgeschlossen werden. Die visuellen Nachweise müssen das umgesetzte Theme aus 05a einschließlich Profil-/Farbmodus und relevanter UI-Zustände abdecken. Native Prüfsysteme dürfen separat vom zentralen Buildrunner betrieben und Nachweise dokumentiert manuell erhoben werden.
|
||
+
|
||
+Arbeitsstand 07.09.2026: Matrix/Bedienfolgen in `docs/plattformmatrix.md`, Eingabeprobe `tests/platform/input.frm`, gemeinsamer Aufruf `tests/support/platform-abnahme.py`. 1.2 ist implementiert und für Windows cross-geprüft, bleibt bis zum tatsächlichen Windows-Konsolenlauf offen. Automatisierte macOS-arm64-Nachweise liegen unter `evidence/2026-09-07-macos-arm64`; sie schließen keine manuelle Terminalzelle. Details und offene Befunde: `verification.md`.
|
||
+
|
||
+RGB-Korrektur nach Benutzerbefund vom 07.09.2026 gehört zu 2.5: keine
|
||
+ANSI-/256-Fallbackfarben; gemeinsame feste RGB-Palette für IDE und BASIC;
|
||
+Fensterflächen ohne durchscheinendes Desktopmuster. Neue Delta-Spezifikationen
|
||
+in `ide-oberflaeche` und `textbildschirm` ersetzen den früheren Fallbackvertrag.
|
||
+Die betroffene visuelle Zielprüfung bleibt bis zur echten Wiederholung offen.
|
||
diff --git a/tests/support/ide-execution-pty.py b/tests/support/ide-execution-pty.py
|
||
index b422070..ee369fa 100644
|
||
--- a/tests/support/ide-execution-pty.py
|
||
+++ b/tests/support/ide-execution-pty.py
|
||
@@ -2,6 +2,7 @@
|
||
"""Headless Unix PTY smoke: real terminal handoff, child Ctrl+C and IDE resume.
|
||
Run after cargo build -p tb-ide: python3 tests/support/ide-execution-pty.py
|
||
"""
|
||
+import argparse
|
||
import fcntl
|
||
import os
|
||
from pathlib import Path
|
||
@@ -13,10 +14,10 @@ import tempfile
|
||
import termios
|
||
import time
|
||
|
||
-binary = Path(__file__).resolve().parents[2] / 'target/debug/tb'
|
||
|
||
|
||
-def exercise(abort=False, file_shell=False):
|
||
+
|
||
+def exercise(binary, transcript_dir=None, abort=False, file_shell=False):
|
||
with tempfile.TemporaryDirectory(prefix='tb-pty-') as directory:
|
||
source = Path(directory) / 'main.bas'
|
||
command = "printf '\\nWAITING_CHILD\\n'; exec sleep 30" if abort else "printf '\\nSHELL_PROOF\\n'; exit 7"
|
||
@@ -32,7 +33,7 @@ def exercise(abort=False, file_shell=False):
|
||
os.dup2(slave, fd)
|
||
os.close(master)
|
||
os.close(slave)
|
||
- os.environ['TERM'] = 'xterm-256color'
|
||
+ os.environ.setdefault('TERM', 'xterm-256color')
|
||
os.environ['XDG_CONFIG_HOME'] = directory
|
||
# Keep the controlling terminal alive while checking restoration.
|
||
signal.signal(signal.SIGINT, lambda *_: None)
|
||
@@ -83,7 +84,17 @@ def exercise(abort=False, file_shell=False):
|
||
transcript.extend(os.read(master, 65536))
|
||
assert transcript.count(b'?1049h') >= 2
|
||
assert transcript.count(b'?1049l') >= 2
|
||
+ # Tatsächliche Ausgabe muss auch bei NO_COLOR/fehlender Erkennung
|
||
+ # dieselben absoluten IDE-Farben enthalten, ohne Palettenänderung.
|
||
+ assert b'38;2;170;170;170' in transcript
|
||
+ assert b'48;2;0;0;170' in transcript
|
||
+ assert b'48;2;170;0;170' in transcript
|
||
+ assert b'38;5;' not in transcript and b'48;5;' not in transcript
|
||
+ assert b'\x1b]' not in transcript
|
||
finally:
|
||
+ if transcript_dir:
|
||
+ name = 'file-shell' if file_shell else 'shell-abort' if abort else 'shell-exit'
|
||
+ (transcript_dir / (name + '.ansi')).write_bytes(transcript)
|
||
if not reaped:
|
||
os.killpg(pid, signal.SIGKILL)
|
||
os.waitpid(pid, 0)
|
||
@@ -91,6 +102,18 @@ def exercise(abort=False, file_shell=False):
|
||
os.close(slave)
|
||
|
||
|
||
-for kwargs in ({}, {'abort': True}, {'file_shell': True}):
|
||
- exercise(**kwargs)
|
||
- print('PASS', kwargs or {'shell_exit': 7})
|
||
+def main():
|
||
+ parser = argparse.ArgumentParser(description=__doc__)
|
||
+ parser.add_argument('--binary', type=Path, default=Path(__file__).resolve().parents[2] / 'target/debug/tb')
|
||
+ parser.add_argument('--transcript-dir', type=Path)
|
||
+ args = parser.parse_args()
|
||
+ binary = args.binary.resolve(strict=True)
|
||
+ if args.transcript_dir:
|
||
+ args.transcript_dir.mkdir(parents=True, exist_ok=True)
|
||
+ for kwargs in ({}, {'abort': True}, {'file_shell': True}):
|
||
+ exercise(binary, args.transcript_dir, **kwargs)
|
||
+ print('PASS', kwargs or {'shell_exit': 7})
|
||
+
|
||
+
|
||
+if __name__ == '__main__':
|
||
+ main()
|