Phase 4: Formulardateien und Konvertierung
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
||||
//!
|
||||
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
@@ -21,20 +22,68 @@ fn main() -> ExitCode {
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("build") => cmd_build(&args[1..]),
|
||||
Some("check") => cmd_check(&args[1..]),
|
||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||
_ => {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
eprintln!(
|
||||
"Aufruf: tbc run|build|check <datei.bas> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||
);
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_convert_frm(args: &[String]) -> ExitCode {
|
||||
if args.len() != 2 {
|
||||
eprintln!("Aufruf: tbc convert-frm <quelle.frm> <ziel.frm>");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
let input = Path::new(&args[0]);
|
||||
let output = Path::new(&args[1]);
|
||||
let bytes = match std::fs::read(input) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {error}", input.display());
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let converted = match tb_ui::frm::read_binary(&input.display().to_string(), &bytes) {
|
||||
Ok(converted) => converted,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let text = tb_ui::frm::write_text(&converted.form);
|
||||
if let Err(error) = std::fs::write(output, text) {
|
||||
eprintln!("{}: {error}", output.display());
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
for warning in &converted.skipped {
|
||||
eprintln!(
|
||||
"{}: Byte 0x{:04x}: {} nicht übernommen",
|
||||
input.display(),
|
||||
warning.offset,
|
||||
warning.name
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
"{}: {} nicht übernommene Binärangaben",
|
||||
input.display(),
|
||||
converted.skipped.len()
|
||||
);
|
||||
println!("{}", output.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn module_name(path: &Path) -> String {
|
||||
path.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_else(|| "MODUL".into())
|
||||
}
|
||||
|
||||
fn compile(path_arg: Option<&String>) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
fn compile(
|
||||
path_arg: Option<&String>,
|
||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
return Err(ExitCode::from(1));
|
||||
@@ -133,7 +182,11 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
eprintln!("STOP in line {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error { code, line, message } => {
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
message,
|
||||
} => {
|
||||
eprintln!("Runtime error {code}: {message} in line {line}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
24
crates/tb-cli/tests/convert_frm.rs
Normal file
24
crates/tb-cli/tests/convert_frm.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn conversion_failure_leaves_no_output() {
|
||||
let dir = std::env::temp_dir().join(format!("tbc-convert-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let input = dir.join("broken.frm");
|
||||
let output = dir.join("converted.frm");
|
||||
std::fs::write(&input, b"not a form").unwrap();
|
||||
|
||||
let result = Command::new(env!("CARGO_BIN_EXE_tbc"))
|
||||
.args([
|
||||
"convert-frm",
|
||||
input.to_str().unwrap(),
|
||||
output.to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.status.success());
|
||||
assert!(!output.exists());
|
||||
assert!(String::from_utf8_lossy(&result.stderr).contains("0x0000"));
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
@@ -68,6 +68,29 @@ impl ObjectClass {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Form => "Form",
|
||||
Self::CheckBox => "CheckBox",
|
||||
Self::ComboBox => "ComboBox",
|
||||
Self::CommandButton => "CommandButton",
|
||||
Self::DirListBox => "DirListBox",
|
||||
Self::DriveListBox => "DriveListBox",
|
||||
Self::FileListBox => "FileListBox",
|
||||
Self::Frame => "Frame",
|
||||
Self::HScrollBar => "HScrollBar",
|
||||
Self::Label => "Label",
|
||||
Self::ListBox => "ListBox",
|
||||
Self::Menu => "Menu",
|
||||
Self::OptionButton => "OptionButton",
|
||||
Self::PictureBox => "PictureBox",
|
||||
Self::TextBox => "TextBox",
|
||||
Self::Timer => "Timer",
|
||||
Self::VScrollBar => "VScrollBar",
|
||||
Self::Screen => "Screen",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(name: &str) -> Option<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
@@ -182,11 +205,12 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
|
||||
range("WIDTH", 1, 1, 254),
|
||||
]);
|
||||
}
|
||||
if !matches!(
|
||||
class,
|
||||
Form | Frame | Label | PictureBox | Timer | Menu | Screen
|
||||
) {
|
||||
p.extend([int("INDEX", 0), int("TABINDEX", 0), boolp("TABSTOP", true)]);
|
||||
if !matches!(class, Form | Timer | Menu | Screen) {
|
||||
p.push(int("INDEX", 0));
|
||||
p.push(int("TABINDEX", 0));
|
||||
}
|
||||
if !matches!(class, Form | Frame | Label | Timer | Menu | Screen) {
|
||||
p.push(boolp("TABSTOP", true));
|
||||
}
|
||||
if !matches!(class, Form | Timer | Menu | Screen) {
|
||||
p.push(string("CTLNAME", ""));
|
||||
|
||||
@@ -30,6 +30,7 @@ impl PropertyValue {
|
||||
PropertyDefault::Boolean(v) => Self::Boolean(v),
|
||||
PropertyDefault::Empty if ty == PropertyType::Object => Self::Object(None),
|
||||
PropertyDefault::Empty if ty == PropertyType::String => Self::String(String::new()),
|
||||
PropertyDefault::Empty if ty == PropertyType::Boolean => Self::Boolean(false),
|
||||
PropertyDefault::Empty => Self::Integer(0),
|
||||
}
|
||||
}
|
||||
|
||||
2042
crates/tb-ui/src/frm.rs
Normal file
2042
crates/tb-ui/src/frm.rs
Normal file
File diff suppressed because it is too large
Load Diff
2687
crates/tb-ui/src/frm_pcode.rs
Normal file
2687
crates/tb-ui/src/frm_pcode.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@
|
||||
|
||||
pub mod events;
|
||||
pub mod forms;
|
||||
pub mod frm;
|
||||
mod frm_pcode;
|
||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
pub mod screen;
|
||||
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
||||
|
||||
16
crates/tb-ui/tests/data/new.frm.hex
Normal file
16
crates/tb-ui/tests/data/new.frm.hex
Normal file
@@ -0,0 +1,16 @@
|
||||
fc 08 01 00 0e 00 a8 01 c3 01 09 00 01 02 03 04
|
||||
06 05 08 0a 69 00 00 00 00 00 00 00 56 00 3d 00
|
||||
00 00 00 22 85 29 00 00 00 00 00 52 29 00 00 03
|
||||
0f 11 3f 00 00 07 00 00 47 00 02 00 00 0f 3d 01
|
||||
03 00 00 c1 00 00 0a 00 00 00 00 00 00 00 0b 08
|
||||
03 0c 00 00 07 00 00 4c 00 00 00 29 00 03 00 4e
|
||||
65 77 08 00 43 6f 6d 6d 61 6e 64 31 5d 00 00 03
|
||||
4e 65 77 00 00 03 08 43 6f 6d 6d 61 6e 64 31 05
|
||||
01 ff ff 24 00 ff ff 56 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 56 00 00 00 04
|
||||
00 09 00 08 00 ff ff ff ff ff ff ff ff 00 00 00
|
||||
00 00 00 03 01
|
||||
Reference in New Issue
Block a user