Phase 5: Ausführung und Output implementieren und archivieren
This commit is contained in:
@@ -2584,55 +2584,123 @@ fn dialog_set(
|
||||
model.set_initial(object, property, value)
|
||||
}
|
||||
|
||||
fn dialog_event(
|
||||
model: &mut FormsModel,
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
plain_access_key: bool,
|
||||
) -> Option<FormEvent> {
|
||||
loop {
|
||||
model.render(screen);
|
||||
/// Fortsetzbarer modaler Dialog; Modell, Fokus und Hintergrund bleiben erhalten.
|
||||
pub struct Dialog {
|
||||
model: FormsModel,
|
||||
old: TextScreen,
|
||||
buttons: Vec<i16>,
|
||||
default: i16,
|
||||
}
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DialogValue {
|
||||
Number(i16),
|
||||
Text(String),
|
||||
}
|
||||
impl Dialog {
|
||||
pub fn poll(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
) -> Option<DialogValue> {
|
||||
if (self.old.cols(), self.old.rows()) != (screen.cols(), screen.rows()) {
|
||||
self.old.resize(screen.cols(), screen.rows());
|
||||
self.model.resize(screen.cols(), screen.rows());
|
||||
}
|
||||
self.model.render(screen);
|
||||
host.present(screen);
|
||||
// One event per poll also bounds arbitrarily long queued input.
|
||||
let event = queued
|
||||
.pop_front()
|
||||
.map(|e| e.ereignis)
|
||||
.or_else(|| host.next_event(true))?;
|
||||
.or_else(|| host.next_event(false))?;
|
||||
match event {
|
||||
Ereignis::Taste(key, shift) => {
|
||||
let shift = if plain_access_key && key.chars().count() == 1 {
|
||||
let shift = if !self.buttons.is_empty() && key.chars().count() == 1 {
|
||||
shift | umschalt::ALT
|
||||
} else {
|
||||
shift
|
||||
};
|
||||
model.handle_key(&key, shift);
|
||||
self.model.handle_key(&key, shift);
|
||||
}
|
||||
Ereignis::Maus(event) => {
|
||||
let now = if model.mouse_needs_time(event) {
|
||||
let now = if self.model.mouse_needs_time(event) {
|
||||
host.jetzt_ms()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
model.handle_mouse_at(event, now);
|
||||
self.model.handle_mouse_at(event, now);
|
||||
}
|
||||
Ereignis::Groesse { cols, rows } => {
|
||||
screen.resize(cols, rows);
|
||||
model.resize(cols, rows);
|
||||
self.old.resize(cols, rows);
|
||||
self.model.resize(cols, rows);
|
||||
}
|
||||
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
|
||||
queued.push_front(event.into());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
while let Some(event) = model.next_event() {
|
||||
if event.name == "CLICK" {
|
||||
return Some(event);
|
||||
while let Some(event) = self.model.next_event() {
|
||||
if event.name != "CLICK" {
|
||||
continue;
|
||||
}
|
||||
let value = if self.buttons.is_empty() {
|
||||
match event.object {
|
||||
3 => DialogValue::Text(self.model.string((2, None), "TEXT")),
|
||||
4 => DialogValue::Text(String::new()),
|
||||
_ => continue,
|
||||
}
|
||||
} else {
|
||||
let Some(code) = event
|
||||
.object
|
||||
.checked_sub(2)
|
||||
.and_then(|i| self.buttons.get(i as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
DialogValue::Number(*code)
|
||||
};
|
||||
*screen = self.old.clone();
|
||||
return Some(value);
|
||||
}
|
||||
None
|
||||
}
|
||||
fn run(
|
||||
&mut self,
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
) -> DialogValue {
|
||||
loop {
|
||||
if let Some(value) = self.poll(screen, host, queued) {
|
||||
return value;
|
||||
}
|
||||
if queued.front().is_some_and(|e| {
|
||||
matches!(
|
||||
e.ereignis,
|
||||
Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)
|
||||
)
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
if !queued.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match host.warten(None) {
|
||||
Some(e) => queued.push_back(e.into()),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
*screen = self.old.clone();
|
||||
if self.buttons.is_empty() {
|
||||
DialogValue::Text(String::new())
|
||||
} else {
|
||||
DialogValue::Number(self.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeigt MSGBOX als echtes modales FormsModel-Formular.
|
||||
pub fn msgbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
@@ -2641,6 +2709,33 @@ pub fn msgbox_dialog(
|
||||
kind: i32,
|
||||
title: &str,
|
||||
) -> Result<i16, RuntimeError> {
|
||||
match message_dialog(screen, text, kind, title)?.run(screen, host, queued) {
|
||||
DialogValue::Number(n) => Ok(n),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
pub fn inputbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
prompt: &str,
|
||||
title: &str,
|
||||
initial: &str,
|
||||
position: Option<(i32, i32)>,
|
||||
) -> Result<String, RuntimeError> {
|
||||
match input_dialog(screen, prompt, title, initial, position)?.run(screen, host, queued) {
|
||||
DialogValue::Text(s) => Ok(s),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Zeigt MSGBOX als echtes modales FormsModel-Formular.
|
||||
pub fn message_dialog(
|
||||
screen: &TextScreen,
|
||||
text: &str,
|
||||
kind: i32,
|
||||
title: &str,
|
||||
) -> Result<Dialog, RuntimeError> {
|
||||
let groups: &[&[(i16, &str)]] = &[
|
||||
&[(1, "&OK")],
|
||||
&[(1, "&OK"), (2, "&Cancel")],
|
||||
@@ -2732,30 +2827,22 @@ pub fn msgbox_dialog(
|
||||
model.show(0, true)?;
|
||||
model.focus((default as u16 + 2, None))?;
|
||||
model.events.clear();
|
||||
let chosen = loop {
|
||||
let Some(event) = dialog_event(&mut model, screen, host, queued, true) else {
|
||||
break buttons[default].0;
|
||||
};
|
||||
if let Some(index) = event.object.checked_sub(2).map(usize::from) {
|
||||
if let Some((code, _)) = buttons.get(index) {
|
||||
break *code;
|
||||
}
|
||||
}
|
||||
};
|
||||
*screen = old;
|
||||
Ok(chosen)
|
||||
Ok(Dialog {
|
||||
model,
|
||||
old,
|
||||
buttons: buttons.iter().map(|b| b.0).collect(),
|
||||
default: buttons[default].0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Zeigt INPUTBOX$ als echtes modales FormsModel-Formular.
|
||||
pub fn inputbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
pub fn input_dialog(
|
||||
screen: &TextScreen,
|
||||
prompt: &str,
|
||||
title: &str,
|
||||
initial: &str,
|
||||
position: Option<(i32, i32)>,
|
||||
) -> Result<String, RuntimeError> {
|
||||
) -> Result<Dialog, RuntimeError> {
|
||||
let width = 46usize.min(screen.cols());
|
||||
let height = 16usize.min(screen.rows());
|
||||
let (left, top) = position.map_or_else(
|
||||
@@ -2824,23 +2911,12 @@ pub fn inputbox_dialog(
|
||||
model.show(0, true)?;
|
||||
model.focus((2, None))?;
|
||||
model.events.clear();
|
||||
let accepted = loop {
|
||||
let Some(event) = dialog_event(&mut model, screen, host, queued, false) else {
|
||||
break false;
|
||||
};
|
||||
match event.object {
|
||||
3 => break true,
|
||||
4 => break false,
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
let text = if accepted {
|
||||
model.string((2, None), "TEXT")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
*screen = old;
|
||||
Ok(text)
|
||||
Ok(Dialog {
|
||||
model,
|
||||
old,
|
||||
buttons: Vec::new(),
|
||||
default: 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,16 +4,12 @@
|
||||
//! Er lebt hier statt in `tb-runtime`, damit die Ausführungsschicht ohne
|
||||
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
|
||||
|
||||
use std::io::{self, Stdout};
|
||||
use std::io::{self, IsTerminal, Stdout};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
@@ -29,6 +25,7 @@ use crate::signale::Signalquelle;
|
||||
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
|
||||
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
|
||||
pub struct TerminalHost {
|
||||
guard: crate::terminal::TerminalGuard<Stdout>,
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
/// Nullpunkt der monotonen Zeit (`Host::jetzt_ms`).
|
||||
start: Instant,
|
||||
@@ -40,15 +37,18 @@ pub struct TerminalHost {
|
||||
|
||||
impl TerminalHost {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
enable_raw_mode()?;
|
||||
let mut out = io::stdout();
|
||||
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
|
||||
return Err(io::Error::other("Kein interaktives Terminal"));
|
||||
}
|
||||
let guard = crate::terminal::TerminalGuard::enter(io::stdout())?;
|
||||
let out = io::stdout();
|
||||
let terminal = Terminal::new(CrosstermBackend::new(out))?;
|
||||
let flaeche = terminal
|
||||
.size()
|
||||
.map(|s| (s.width as usize, s.height as usize))
|
||||
.unwrap_or((80, 25));
|
||||
Ok(TerminalHost {
|
||||
guard,
|
||||
terminal,
|
||||
start: Instant::now(),
|
||||
signale: Signalquelle::neu(),
|
||||
@@ -63,23 +63,25 @@ impl TerminalHost {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalHost {
|
||||
fn drop(&mut self) {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
let _ = self.terminal.show_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for TerminalHost {
|
||||
fn shell(&mut self, command: &str) -> Result<Option<i32>, tb_runtime::errors::RuntimeError> {
|
||||
let result = self.guard.shell(command, &self.signale);
|
||||
let _ = self.terminal.clear();
|
||||
result
|
||||
.map(Some)
|
||||
.map_err(|_| tb_runtime::errors::RuntimeError(53))
|
||||
}
|
||||
fn present(&mut self, screen: &TextScreen) {
|
||||
let _ = self
|
||||
.terminal
|
||||
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
|
||||
let _ = self.terminal.draw(|f| {
|
||||
if f.area().width < 80 || f.area().height < 25 {
|
||||
f.render_widget(
|
||||
ratatui::widgets::Paragraph::new("Terminal zu klein: Minimum 80x25"),
|
||||
f.area(),
|
||||
);
|
||||
} else {
|
||||
f.render_widget(ScreenWidget(screen), f.area());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
@@ -152,7 +154,7 @@ impl Host for TerminalHost {
|
||||
/// crossterm zählt ab 0, der Dialekt ab 1 — umgerechnet wird hier, damit es
|
||||
/// in der Laufzeit nur eine Zählweise gibt. Ereignisse außerhalb der
|
||||
/// Darstellungsfläche und Ereignisse ohne Entsprechung liefern `None`.
|
||||
fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
|
||||
pub fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
|
||||
let (cols, rows) = flaeche;
|
||||
let spalte = m.column as usize + 1;
|
||||
let zeile = m.row as usize + 1;
|
||||
@@ -195,7 +197,7 @@ fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis>
|
||||
}
|
||||
|
||||
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
|
||||
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
pub fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
// Strg+Untbr bzw. Strg+C: Abbruchwunsch.
|
||||
if k.modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& matches!(k.code, KeyCode::Char('c') | KeyCode::Pause)
|
||||
|
||||
@@ -14,3 +14,6 @@ pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
pub mod screen;
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
||||
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod terminal;
|
||||
|
||||
@@ -22,20 +22,9 @@ pub fn basic_color(n: u8) -> Color {
|
||||
/// Zeichenbares Gegenstück zu [`TextScreen`].
|
||||
pub struct ScreenWidget<'a>(pub &'a TextScreen);
|
||||
|
||||
/// Rendert den Bildschirm oben links in die verfügbare Fläche. Ist das
|
||||
/// Terminal kleiner als die Mindestgröße, erscheint nur ein Hinweis
|
||||
/// (btop-artig); die Anwendung ruft bei Resize `TextScreen::resize` auf,
|
||||
/// damit Puffer und Terminal deckungsgleich bleiben.
|
||||
/// Clipped viewport; only the terminal owner enforces physical minimum size.
|
||||
impl Widget for ScreenWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if (area.width as usize) < MIN_COLS || (area.height as usize) < MIN_ROWS {
|
||||
let msg = format!(
|
||||
"Terminal zu klein: {}x{} — Minimum {}x{}",
|
||||
area.width, area.height, MIN_COLS, MIN_ROWS
|
||||
);
|
||||
buf.set_string(area.x, area.y, msg, Style::default().fg(Color::Red));
|
||||
return;
|
||||
}
|
||||
let cols = self.0.cols().min(area.width as usize);
|
||||
let rows = self.0.rows().min(area.height as usize);
|
||||
for row in 0..rows {
|
||||
|
||||
190
crates/tb-ui/src/terminal.rs
Normal file
190
crates/tb-ui/src/terminal.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use crossterm::{
|
||||
cursor::{Hide, Show},
|
||||
event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
|
||||
execute,
|
||||
style::ResetColor,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Ein Besitzer für Raw Mode und Terminalzustand, auch bei teilweiser Initialisierung.
|
||||
pub struct TerminalGuard<W: Write> {
|
||||
writer: W,
|
||||
raw: fn(bool) -> io::Result<()>,
|
||||
}
|
||||
impl<W: Write> TerminalGuard<W> {
|
||||
pub fn enter(writer: W) -> io::Result<Self> {
|
||||
Self::with_raw(writer, |on| {
|
||||
if on {
|
||||
enable_raw_mode()
|
||||
} else {
|
||||
disable_raw_mode()
|
||||
}
|
||||
})
|
||||
}
|
||||
fn with_raw(writer: W, raw: fn(bool) -> io::Result<()>) -> io::Result<Self> {
|
||||
let mut guard = Self { writer, raw };
|
||||
guard.resume()?;
|
||||
Ok(guard)
|
||||
}
|
||||
fn resume(&mut self) -> io::Result<()> {
|
||||
(self.raw)(true)?;
|
||||
execute!(
|
||||
self.writer,
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
EnableBracketedPaste,
|
||||
Hide
|
||||
)
|
||||
}
|
||||
fn suspend(&mut self) -> io::Result<()> {
|
||||
(self.raw)(false)?;
|
||||
execute!(
|
||||
self.writer,
|
||||
ResetColor,
|
||||
Show,
|
||||
DisableMouseCapture,
|
||||
DisableBracketedPaste,
|
||||
LeaveAlternateScreen
|
||||
)
|
||||
}
|
||||
/// Foreground child owns cooked terminal input. Always restore the IDE,
|
||||
/// including spawn/wait errors and a child interrupted by Ctrl+C.
|
||||
pub fn shell(
|
||||
&mut self,
|
||||
command: &str,
|
||||
signals: &crate::signale::Signalquelle,
|
||||
) -> io::Result<i32> {
|
||||
let result = self.shell_with_interrupt(command, || signals.abholen().is_some());
|
||||
// A child may exit before try_wait observes the terminal signal.
|
||||
// It belongs to the handed-off terminal, not the resumed BASIC run.
|
||||
while signals.abholen().is_some() {}
|
||||
result
|
||||
}
|
||||
fn shell_with_interrupt(
|
||||
&mut self,
|
||||
command: &str,
|
||||
mut interrupted: impl FnMut() -> bool,
|
||||
) -> io::Result<i32> {
|
||||
let result = self.suspend().and_then(|_| {
|
||||
let mut child = tb_runtime::host::shell_command(command).spawn()?;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => return Ok(status.code().unwrap_or(0)),
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if interrupted() {
|
||||
let _ = child.kill();
|
||||
return child.wait().map(|status| status.code().unwrap_or(0));
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
});
|
||||
let restored = self.resume();
|
||||
restored.and(result)
|
||||
}
|
||||
}
|
||||
impl<W: Write> Drop for TerminalGuard<W> {
|
||||
fn drop(&mut self) {
|
||||
let _ = (self.raw)(false);
|
||||
let _ = execute!(
|
||||
self.writer,
|
||||
ResetColor,
|
||||
Show,
|
||||
DisableMouseCapture,
|
||||
DisableBracketedPaste,
|
||||
LeaveAlternateScreen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicI32, Ordering},
|
||||
};
|
||||
static RAW: AtomicI32 = AtomicI32::new(0);
|
||||
#[derive(Clone)]
|
||||
struct Output(Rc<RefCell<Vec<u8>>>);
|
||||
impl Write for Output {
|
||||
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
|
||||
self.0.borrow_mut().extend_from_slice(b);
|
||||
Ok(b.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn raw(on: bool) -> io::Result<()> {
|
||||
RAW.store(i32::from(on), Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn terminal_cleanup_on_normal_error_and_partial_initialization() {
|
||||
for fail in [false, true] {
|
||||
let out = Output(Rc::default());
|
||||
let copy = out.clone();
|
||||
let result = (|| -> io::Result<()> {
|
||||
let _guard = TerminalGuard::with_raw(out, raw)?;
|
||||
if fail {
|
||||
return Err(io::Error::other("Renderfehler"));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
assert_eq!(result.is_err(), fail);
|
||||
assert_eq!(RAW.load(Ordering::SeqCst), 0);
|
||||
let s = String::from_utf8(copy.0.borrow().clone()).unwrap();
|
||||
assert!(s.contains("?1049h") && s.contains("?1049l") && s.contains("?25h"));
|
||||
}
|
||||
for (command, abort, expected) in [
|
||||
("exit 0", false, 0),
|
||||
("exit 7", false, 7),
|
||||
(
|
||||
if cfg!(windows) {
|
||||
"ping -n 30 127.0.0.1 >nul"
|
||||
} else {
|
||||
"exec sleep 30"
|
||||
},
|
||||
true,
|
||||
0,
|
||||
),
|
||||
] {
|
||||
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
|
||||
);
|
||||
assert_eq!(RAW.load(Ordering::SeqCst), 1);
|
||||
let text = String::from_utf8(copy.0.borrow().clone()).unwrap();
|
||||
assert_eq!(text.matches("?1049h").count(), 2);
|
||||
assert_eq!(text.matches("?1049l").count(), 1);
|
||||
drop(guard);
|
||||
assert_eq!(RAW.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
let out = Output(Rc::default());
|
||||
let copy = out.clone();
|
||||
assert!(TerminalGuard::with_raw(out, |on| {
|
||||
raw(on)?;
|
||||
if on {
|
||||
Err(io::Error::other("Raw-Fehler"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(RAW.load(Ordering::SeqCst), 0);
|
||||
assert!(String::from_utf8(copy.0.borrow().clone())
|
||||
.unwrap()
|
||||
.contains("?25h"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user