Phase 5: Ausführung und Output implementieren und archivieren
This commit is contained in:
@@ -21,8 +21,12 @@ pub enum Mode {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Execution {
|
||||
Idle,
|
||||
Compiling,
|
||||
Paused,
|
||||
Running,
|
||||
Waiting,
|
||||
Ended,
|
||||
Error,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WindowKind {
|
||||
@@ -213,6 +217,9 @@ pub enum DialogKind {
|
||||
Dirty(AfterSave),
|
||||
LoadText,
|
||||
SaveText,
|
||||
Print,
|
||||
CommandLine,
|
||||
ResumeRevision,
|
||||
Display,
|
||||
Paths,
|
||||
RightMouse,
|
||||
@@ -256,6 +263,7 @@ pub enum Hit {
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub session: crate::execution::Session,
|
||||
pub editor: crate::editor::Editor,
|
||||
pub project: Project,
|
||||
pub options: Options,
|
||||
@@ -293,6 +301,7 @@ impl App {
|
||||
let (options, errors, config_disk) = Options::load(&config_path);
|
||||
project.include_paths = options.include_paths.clone();
|
||||
let mut app = Self {
|
||||
session: Default::default(),
|
||||
editor: Default::default(),
|
||||
project,
|
||||
options: options.clone(),
|
||||
@@ -423,6 +432,7 @@ impl App {
|
||||
)
|
||||
.trim_end()
|
||||
.to_string(),
|
||||
WindowKind::Output if self.session.old_revision => "Output · ALTES KOMPILAT".into(),
|
||||
_ => format!("{:?}", w.kind),
|
||||
}
|
||||
}
|
||||
@@ -452,6 +462,9 @@ impl App {
|
||||
return Some(format!("Fachfunktion folgt in Phase-5-Change {phase:02}"));
|
||||
}
|
||||
use Command::*;
|
||||
if command == Shell && self.session.host.shell_request.is_some() {
|
||||
return Some("Shell-Übergabe bereits angefordert".into());
|
||||
}
|
||||
if matches!(
|
||||
command,
|
||||
Undo | Cut | Paste | Clear | LoadText | NewSub | NewFunction | Replace
|
||||
@@ -467,6 +480,7 @@ impl App {
|
||||
if matches!(
|
||||
command,
|
||||
LoadText
|
||||
| Print
|
||||
| SaveText
|
||||
| Cut
|
||||
| Copy
|
||||
@@ -532,6 +546,40 @@ impl App {
|
||||
use Command::*;
|
||||
let id = self.active_document();
|
||||
match command {
|
||||
Start => self.start_execution()?,
|
||||
Restart => self.restart_target()?,
|
||||
Continue => self.continue_execution()?,
|
||||
Pause => self.pause_execution(),
|
||||
OutputScreen => {
|
||||
self.session.fullscreen = !self.session.fullscreen;
|
||||
}
|
||||
CommandLine => self.open_dialog(
|
||||
"COMMAND$",
|
||||
DialogKind::CommandLine,
|
||||
vec![Field::text(
|
||||
"Argumente für den nächsten Start",
|
||||
&self.session.command,
|
||||
)],
|
||||
),
|
||||
Shell => {
|
||||
self.session.file_shell = true;
|
||||
self.session.host.shell_request = Some(String::new());
|
||||
}
|
||||
Print => self.open_dialog(
|
||||
"Print",
|
||||
DialogKind::Print,
|
||||
vec![
|
||||
Field::text("UTF-8-Ausgabedatei", self.output_default("LPT1.TXT")),
|
||||
Field::toggle("Bestehendes Ziel überschreiben", false),
|
||||
Field::toggle(
|
||||
"Nur Auswahl",
|
||||
self.project
|
||||
.view(self.editor_view()?)?
|
||||
.selection()
|
||||
.is_some(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Cut | Copy | Paste | Clear | NewSub | NewFunction | IncludedFile | IncludedLines
|
||||
| Find | SelectedText | FindNext | Replace | Procedures | PreviousCode
|
||||
| Diagnostics => self.editor_command(command)?,
|
||||
@@ -706,6 +754,9 @@ impl App {
|
||||
_ => WindowKind::Project,
|
||||
};
|
||||
self.show_tool(kind);
|
||||
if command == Output {
|
||||
self.session.fullscreen = false;
|
||||
}
|
||||
}
|
||||
MenuBar => {
|
||||
self.properties = !self.properties;
|
||||
@@ -856,7 +907,7 @@ impl App {
|
||||
let offset = (self.windows.len() % 5) as u16 * 2;
|
||||
clamp(Rect::new(offset, 1 + offset, 60, 16), self.area())
|
||||
}
|
||||
fn show_tool(&mut self, kind: WindowKind) {
|
||||
pub(crate) fn show_tool(&mut self, kind: WindowKind) {
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.kind == kind) {
|
||||
self.active = w.id;
|
||||
w.state = WindowState::Normal;
|
||||
@@ -932,6 +983,8 @@ impl App {
|
||||
match after {
|
||||
AfterSave::Stay => return Ok(()),
|
||||
AfterSave::Exit => {
|
||||
self.session.finish();
|
||||
self.basic_events.clear();
|
||||
self.quit = true;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -943,6 +996,9 @@ impl App {
|
||||
self.project.open_project(&path, Decision::Discard)?;
|
||||
}
|
||||
}
|
||||
self.session = Default::default();
|
||||
self.basic_events.clear();
|
||||
self.execution = Execution::Idle;
|
||||
self.base = self.project.directory().to_path_buf();
|
||||
self.editor = Default::default();
|
||||
self.windows.clear();
|
||||
@@ -1141,11 +1197,25 @@ impl App {
|
||||
let text = std::fs::read_to_string(self.base.join(d.fields[0].string()))?;
|
||||
self.editor_insert(&text)?;
|
||||
}
|
||||
DialogKind::SaveText => {
|
||||
DialogKind::CommandLine => {
|
||||
self.session.command = d.fields[0].string();
|
||||
}
|
||||
DialogKind::ResumeRevision => {
|
||||
if d.fields[0].index() == 0 {
|
||||
self.restart_target()?;
|
||||
} else {
|
||||
self.resume_execution(true);
|
||||
}
|
||||
}
|
||||
DialogKind::SaveText | DialogKind::Print => {
|
||||
let (id, _) = self.code_cursor()?;
|
||||
self.project.save_text(
|
||||
id,
|
||||
self.project.view(self.editor_view()?)?.selection(),
|
||||
if matches!(d.kind, DialogKind::Print) && !d.fields[2].flag() {
|
||||
None
|
||||
} else {
|
||||
self.project.view(self.editor_view()?)?.selection()
|
||||
},
|
||||
&Destination {
|
||||
path: d.fields[0].string().into(),
|
||||
overwrite: d.fields[1].flag(),
|
||||
@@ -1255,8 +1325,20 @@ impl App {
|
||||
self.line_leave(old);
|
||||
}
|
||||
fn handle_event(&mut self, event: Event) {
|
||||
if matches!(event, Event::Key(k) if k.kind != KeyEventKind::Release && k.code == K::Pause && k.modifiers.contains(M::CONTROL))
|
||||
{
|
||||
self.pause_execution();
|
||||
return;
|
||||
}
|
||||
if let Event::Resize(w, h) = event {
|
||||
self.size = (w, h);
|
||||
if let Some(vm) = self.session.vm.as_mut() {
|
||||
vm.rt.ereignis(tb_runtime::host::Ereignis::Groesse {
|
||||
cols: w as usize,
|
||||
rows: h as usize,
|
||||
});
|
||||
vm.forms.resize(w as usize, h as usize);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.size.0 < 80 || self.size.1 < 25 {
|
||||
@@ -1269,6 +1351,10 @@ impl App {
|
||||
self.key(key);
|
||||
} else if let Event::Mouse(mouse) = event {
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() {
|
||||
if self.session.fullscreen {
|
||||
self.basic_events.push(Event::Mouse(mouse));
|
||||
return;
|
||||
}
|
||||
if let Some(w) = self.active_window() {
|
||||
let r = self.rect(w);
|
||||
let inner = Rect::new(
|
||||
@@ -1278,6 +1364,9 @@ impl App {
|
||||
r.height.saturating_sub(2),
|
||||
);
|
||||
if inner.contains((mouse.column, mouse.row).into()) {
|
||||
let mut mouse = mouse;
|
||||
mouse.column -= inner.x;
|
||||
mouse.row -= inner.y;
|
||||
self.basic_events.push(Event::Mouse(mouse));
|
||||
return;
|
||||
}
|
||||
@@ -1399,13 +1488,18 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn program_focus(&self) -> bool {
|
||||
self.execution == Execution::Running
|
||||
&& matches!(
|
||||
self.active_window().map(|w| w.kind),
|
||||
Some(WindowKind::Output)
|
||||
)
|
||||
matches!(self.execution, Execution::Running | Execution::Waiting)
|
||||
&& (self.session.fullscreen
|
||||
|| matches!(
|
||||
self.active_window().map(|w| w.kind),
|
||||
Some(WindowKind::Output)
|
||||
))
|
||||
}
|
||||
fn key(&mut self, key: KeyEvent) {
|
||||
if self.session.fullscreen && self.dialog.is_none() && key.code == K::F(4) {
|
||||
self.execute(Command::OutputScreen);
|
||||
return;
|
||||
}
|
||||
if self.editor.chord.is_some()
|
||||
&& self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
@@ -1586,7 +1680,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
if self.program_focus() && key.modifiers.contains(M::CONTROL) && key.code == K::Char('c') {
|
||||
self.basic_events.push(Event::Key(key));
|
||||
self.pause_execution();
|
||||
return;
|
||||
}
|
||||
if let Some(command) = shortcut(key) {
|
||||
@@ -1634,7 +1728,8 @@ impl App {
|
||||
DialogKind::OpenProject
|
||||
| DialogKind::AddFile
|
||||
| DialogKind::LoadText
|
||||
| DialogKind::SaveText => index == 0,
|
||||
| DialogKind::SaveText
|
||||
| DialogKind::Print => index == 0,
|
||||
DialogKind::Save { .. } => index % 2 == 0,
|
||||
DialogKind::Export(_) => index == 3,
|
||||
_ => false,
|
||||
|
||||
@@ -41,6 +41,7 @@ pub enum Command {
|
||||
Start,
|
||||
Restart,
|
||||
Continue,
|
||||
Pause,
|
||||
CommandLine,
|
||||
MakeExe,
|
||||
MakeLibrary,
|
||||
@@ -100,7 +101,6 @@ impl Command {
|
||||
pub fn feature_phase(self) -> Option<u8> {
|
||||
use Command::*;
|
||||
match self {
|
||||
Print | Shell | Start | Restart | Continue | CommandLine | OutputScreen => Some(4),
|
||||
Events | Grid | Palette | MenuDesign | Toolbox | Tool(_) => Some(5),
|
||||
NextStatement | AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches
|
||||
| Trace | History | Breakpoint | ClearBreakpoints | BreakErrors | SetStatement
|
||||
@@ -251,6 +251,7 @@ pub fn menus(designer: bool) -> Vec<Menu> {
|
||||
item("&Start", Start),
|
||||
item("&Restart", Restart),
|
||||
item("&Continue", Continue),
|
||||
item("&Pause (Ctrl+Break)", Pause),
|
||||
item("Modify COMMAND&$…", CommandLine),
|
||||
sep(),
|
||||
item("Make &EXE File…", MakeExe),
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct Search {
|
||||
pub document: Option<DocumentId>,
|
||||
pub wrap: bool,
|
||||
}
|
||||
pub(crate) type Revision = (ProjectStamp, Vec<SourceUnit>, Vec<String>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Editor {
|
||||
pub clipboard: String,
|
||||
@@ -37,7 +39,7 @@ pub struct Editor {
|
||||
pub checked: BTreeMap<DocumentId, u64>,
|
||||
pub compiler: ProjectCompiler,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
revision: Option<(ProjectStamp, Vec<SourceUnit>, Vec<String>)>,
|
||||
pub(crate) revision: Option<Revision>,
|
||||
compiled: Option<CompiledModule>,
|
||||
}
|
||||
|
||||
@@ -789,20 +791,12 @@ impl App {
|
||||
self.editor.revision = None;
|
||||
self.editor.diagnostics.clear();
|
||||
let sources = self.project.sources()?;
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for f in &sources.forms {
|
||||
catalog.append(&f.catalog());
|
||||
}
|
||||
self.editor.revision = Some((
|
||||
ProjectStamp::capture(&self.project),
|
||||
sources.units.clone(),
|
||||
sources.forms.iter().map(tb_ui::frm::write_text).collect(),
|
||||
));
|
||||
match self
|
||||
.editor
|
||||
.compiler
|
||||
.compile("IDE", &sources.units, &catalog, &sources.forms)
|
||||
{
|
||||
match sources.compile(&mut self.editor.compiler, "IDE") {
|
||||
Ok(code) => {
|
||||
self.editor.compiled = Some(code);
|
||||
Ok(self.editor.compiled.as_ref().unwrap())
|
||||
|
||||
327
crates/tb-ide/src/execution.rs
Normal file
327
crates/tb-ide/src/execution.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
//! Cooperative session owned by the IDE event loop, on the VM's thread.
|
||||
use crate::app::{App, DialogKind, Execution, Field, WindowKind};
|
||||
use anyhow::{anyhow, Result};
|
||||
use crossterm::event::Event;
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
use tb_runtime::{
|
||||
errors::RuntimeError,
|
||||
host::{Ereignis, Host},
|
||||
screen::TextScreen,
|
||||
};
|
||||
use tb_vm::{
|
||||
interp::{PollResult, RunEvent, Vm},
|
||||
project_io::{load_program, new_execution, run_target},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IdeHost {
|
||||
pub events: VecDeque<Ereignis>,
|
||||
pub now: u64,
|
||||
pub shell_request: Option<String>,
|
||||
pub shell_result: Option<Result<i32, RuntimeError>>,
|
||||
}
|
||||
impl Host for IdeHost {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
|
||||
self.events.pop_front()
|
||||
}
|
||||
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
|
||||
panic!("IDE poll must never block")
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.now
|
||||
}
|
||||
fn shell(&mut self, command: &str) -> Result<Option<i32>, RuntimeError> {
|
||||
if let Some(result) = self.shell_result.take() {
|
||||
result.map(Some)
|
||||
} else {
|
||||
self.shell_request.get_or_insert_with(|| command.into());
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct Session {
|
||||
pub vm: Option<Vm>,
|
||||
pub host: IdeHost,
|
||||
pub command: String,
|
||||
pub target: Option<PathBuf>,
|
||||
pub project_target: bool,
|
||||
pub old_revision: bool,
|
||||
pub fullscreen: bool,
|
||||
pub forms_phase: bool,
|
||||
pub file_shell: bool,
|
||||
revision: Vec<u8>,
|
||||
source_revision: Option<crate::editor::Revision>,
|
||||
output: TextScreen,
|
||||
}
|
||||
impl Default for Session {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vm: None,
|
||||
host: IdeHost::default(),
|
||||
command: String::new(),
|
||||
target: None,
|
||||
project_target: true,
|
||||
old_revision: false,
|
||||
fullscreen: false,
|
||||
forms_phase: false,
|
||||
file_shell: false,
|
||||
revision: Vec::new(),
|
||||
source_revision: None,
|
||||
output: TextScreen::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Session {
|
||||
pub fn screen(&self) -> &TextScreen {
|
||||
self.vm.as_ref().map_or(&self.output, |vm| &vm.rt.screen)
|
||||
}
|
||||
pub fn finish(&mut self) {
|
||||
if let Some(mut vm) = self.vm.take() {
|
||||
self.output = std::mem::take(&mut vm.rt.screen);
|
||||
}
|
||||
self.host.events.clear();
|
||||
self.host.shell_request = None;
|
||||
self.host.shell_result = None;
|
||||
self.file_shell = false;
|
||||
}
|
||||
}
|
||||
impl App {
|
||||
pub fn start_execution(&mut self) -> Result<()> {
|
||||
self.reset_execution(None, None, true)
|
||||
}
|
||||
fn reset_execution(
|
||||
&mut self,
|
||||
target: Option<PathBuf>,
|
||||
line: Option<u32>,
|
||||
project: bool,
|
||||
) -> Result<()> {
|
||||
self.execution = Execution::Compiling;
|
||||
let result = (|| {
|
||||
let module = if project {
|
||||
self.compile_current()?.clone()
|
||||
} else {
|
||||
load_program(
|
||||
target
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("Kein Ausführungsziel"))?,
|
||||
)
|
||||
.map_err(|e| anyhow!(e))?
|
||||
};
|
||||
let revision = module.to_tbc();
|
||||
let vm = new_execution(
|
||||
module,
|
||||
&self.session.command,
|
||||
Some((self.size.0 as usize, self.size.1 as usize)),
|
||||
line,
|
||||
)?;
|
||||
let target = if project {
|
||||
self.project.path().map(PathBuf::from).or_else(|| {
|
||||
self.project
|
||||
.members()
|
||||
.first()
|
||||
.and_then(|id| self.project.document(*id).ok())
|
||||
.map(|d| d.source_path().to_path_buf())
|
||||
})
|
||||
} else {
|
||||
target
|
||||
};
|
||||
self.session.finish();
|
||||
self.basic_events.clear();
|
||||
self.session.vm = Some(vm);
|
||||
self.session.target = target;
|
||||
self.session.project_target = project;
|
||||
self.session.revision = revision;
|
||||
self.session.source_revision = if project {
|
||||
self.editor.revision.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.session.old_revision = false;
|
||||
self.session.forms_phase = false;
|
||||
self.execution = Execution::Running;
|
||||
self.show_tool(WindowKind::Output);
|
||||
self.message = "Running".into();
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(e) = &result {
|
||||
self.session.finish();
|
||||
self.execution = Execution::Error;
|
||||
self.message = format!("{e:#}");
|
||||
}
|
||||
result
|
||||
}
|
||||
pub fn pause_execution(&mut self) {
|
||||
if matches!(self.execution, Execution::Running | Execution::Waiting) {
|
||||
self.execution = Execution::Paused;
|
||||
self.editor.chord = None;
|
||||
self.message = self.session.vm.as_ref().map_or_else(
|
||||
|| "Paused".into(),
|
||||
|vm| {
|
||||
format!(
|
||||
"Paused · {}:{} · F5: Continue",
|
||||
vm.current_file(),
|
||||
vm.current_line()
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn continue_execution(&mut self) -> Result<()> {
|
||||
if matches!(self.execution, Execution::Running | Execution::Waiting) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.execution != Execution::Paused {
|
||||
return self.start_execution();
|
||||
}
|
||||
let current = if self.session.project_target {
|
||||
self.compile_current().map(|m| m.to_tbc())
|
||||
} else {
|
||||
self.session
|
||||
.target
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("Kein Ausführungsziel"))
|
||||
.and_then(|p| load_program(p).map_err(|e| anyhow!(e)))
|
||||
.map(|m| m.to_tbc())
|
||||
};
|
||||
if current
|
||||
.as_ref()
|
||||
.is_ok_and(|bytes| *bytes == self.session.revision)
|
||||
&& (!self.session.project_target
|
||||
|| self.editor.revision == self.session.source_revision)
|
||||
{
|
||||
self.resume_execution(false);
|
||||
} else {
|
||||
self.open_dialog(
|
||||
"Quellstand geändert",
|
||||
DialogKind::ResumeRevision,
|
||||
vec![Field::choice(
|
||||
"Fortsetzen",
|
||||
vec![
|
||||
"Neu starten (aktueller Quellstand)".into(),
|
||||
"Altes Kompilat ausdrücklich fortsetzen".into(),
|
||||
],
|
||||
0,
|
||||
)],
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn resume_execution(&mut self, old: bool) {
|
||||
if let Some(vm) = self.session.vm.as_mut() {
|
||||
vm.rt.abbruch = false;
|
||||
self.session.old_revision = old;
|
||||
self.execution = Execution::Running;
|
||||
self.message = if old {
|
||||
"Running · ALTES KOMPILAT"
|
||||
} else {
|
||||
"Running"
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
pub fn restart_target(&mut self) -> Result<()> {
|
||||
self.reset_execution(
|
||||
self.session.target.clone(),
|
||||
None,
|
||||
self.session.project_target,
|
||||
)
|
||||
}
|
||||
/// One bounded slice. Tests supply virtual milliseconds, main uses Instant.
|
||||
pub fn tick_execution(&mut self, now: u64) {
|
||||
self.session.host.now = now;
|
||||
if !matches!(self.execution, Execution::Running | Execution::Waiting) {
|
||||
return;
|
||||
}
|
||||
for event in std::mem::take(&mut self.basic_events) {
|
||||
match event {
|
||||
Event::Key(k) => {
|
||||
if let Some(e) = tb_ui::host::taste_zu_ereignis(k) {
|
||||
self.session.host.events.push_back(e);
|
||||
}
|
||||
}
|
||||
Event::Mouse(m) => {
|
||||
if let Some(e) = tb_ui::host::maus_zu_ereignis(
|
||||
m,
|
||||
(self.size.0 as usize, self.size.1 as usize),
|
||||
) {
|
||||
self.session.host.events.push_back(e);
|
||||
}
|
||||
}
|
||||
Event::Paste(s) => {
|
||||
self.session.host.events.extend(s.chars().map(|c| {
|
||||
Ereignis::Taste(
|
||||
if c == '\n' {
|
||||
"\r".into()
|
||||
} else {
|
||||
c.to_string()
|
||||
},
|
||||
0,
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let Some(vm) = self.session.vm.as_mut() else {
|
||||
return;
|
||||
};
|
||||
vm.rt.pump(&mut self.session.host, false);
|
||||
let result = if self.session.forms_phase {
|
||||
vm.poll_visible_forms(&mut self.session.host, 4096)
|
||||
} else {
|
||||
vm.poll(&mut self.session.host, 4096)
|
||||
};
|
||||
match result {
|
||||
PollResult::Yield => self.execution = Execution::Running,
|
||||
PollResult::Waiting { .. } => self.execution = Execution::Waiting,
|
||||
PollResult::Event(event) => match event {
|
||||
RunEvent::Ended
|
||||
if !self.session.forms_phase
|
||||
&& vm.forms.has_visible_forms()
|
||||
&& !vm.is_terminated() =>
|
||||
{
|
||||
self.session.forms_phase = true;
|
||||
self.execution = Execution::Waiting;
|
||||
}
|
||||
RunEvent::Ended => {
|
||||
self.session.finish();
|
||||
self.execution = Execution::Ended;
|
||||
self.message = "Ended".into();
|
||||
}
|
||||
RunEvent::Stopped { .. }
|
||||
| RunEvent::Interrupted { .. }
|
||||
| RunEvent::Breakpoint { .. }
|
||||
| RunEvent::Stepped { .. } => self.pause_execution(),
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
message,
|
||||
} => {
|
||||
self.session.finish();
|
||||
self.execution = Execution::Error;
|
||||
self.message = format!("Runtime error {code} in Zeile {line}: {message}");
|
||||
}
|
||||
RunEvent::Restart { program, line } => {
|
||||
let target = program
|
||||
.as_deref()
|
||||
.map(|p| run_target(self.session.target.as_deref().unwrap(), p))
|
||||
.transpose();
|
||||
match target {
|
||||
Ok(target) => {
|
||||
let project = self.session.project_target
|
||||
&& (target.is_none() || target == self.session.target);
|
||||
let target = target.or_else(|| self.session.target.clone());
|
||||
let _ = self.reset_execution(target, line, project);
|
||||
}
|
||||
Err(error) => {
|
||||
self.session.finish();
|
||||
self.execution = Execution::Error;
|
||||
self.message = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,5 @@ pub mod render;
|
||||
pub mod terminal;
|
||||
|
||||
pub mod editor;
|
||||
|
||||
pub mod execution;
|
||||
|
||||
@@ -5,7 +5,11 @@ use std::{
|
||||
io::{self, IsTerminal},
|
||||
path::PathBuf,
|
||||
};
|
||||
use tb_ide::{app::App, options, terminal::TerminalGuard};
|
||||
use tb_ide::{
|
||||
app::{App, Execution},
|
||||
options,
|
||||
terminal::TerminalGuard,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
@@ -23,11 +27,58 @@ fn main() -> Result<()> {
|
||||
if let Some(path) = args.first() {
|
||||
app.load_initial_project(PathBuf::from(path))?;
|
||||
}
|
||||
let _guard = TerminalGuard::enter(io::stdout())?;
|
||||
let mut guard = TerminalGuard::enter(io::stdout())?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
|
||||
let start = std::time::Instant::now();
|
||||
let mut last_draw = start - std::time::Duration::from_millis(16);
|
||||
let signals = tb_ui::signale::Signalquelle::neu();
|
||||
while !app.quit {
|
||||
terminal.draw(|f| app.render(f))?;
|
||||
app.handle(event::read()?);
|
||||
if signals.abholen().is_some() {
|
||||
app.pause_execution();
|
||||
}
|
||||
let shell = if app.session.file_shell
|
||||
|| matches!(app.execution, Execution::Running | Execution::Waiting)
|
||||
{
|
||||
app.session.host.shell_request.take()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(command) = shell {
|
||||
let result = guard.shell(&command, &signals);
|
||||
terminal.clear()?;
|
||||
let (cols, rows) = crossterm::terminal::size()?;
|
||||
app.handle(event::Event::Resize(cols, rows));
|
||||
if app.session.file_shell {
|
||||
app.session.file_shell = false;
|
||||
app.message = match result {
|
||||
Ok(code) => format!("Shell beendet: {code}"),
|
||||
Err(e) => format!("Shell: {e}"),
|
||||
};
|
||||
} else {
|
||||
app.session.host.shell_result =
|
||||
Some(result.map_err(|_| tb_runtime::errors::RuntimeError(53)));
|
||||
}
|
||||
}
|
||||
if last_draw.elapsed() >= std::time::Duration::from_millis(16) {
|
||||
terminal.draw(|f| app.render(f))?;
|
||||
last_draw = std::time::Instant::now();
|
||||
}
|
||||
// Bounded input batch: both keyboard and VM make progress under load.
|
||||
for _ in 0..64 {
|
||||
if !event::poll(std::time::Duration::ZERO)? {
|
||||
break;
|
||||
}
|
||||
app.handle(event::read()?);
|
||||
}
|
||||
app.tick_execution(start.elapsed().as_millis() as u64);
|
||||
let timeout = if app.execution == Execution::Running {
|
||||
std::time::Duration::ZERO
|
||||
} else {
|
||||
std::time::Duration::from_millis(10)
|
||||
};
|
||||
if event::poll(timeout)? {
|
||||
app.handle(event::read()?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ impl App {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if self.session.fullscreen && self.dialog.is_none() && self.menu.is_none() {
|
||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), area);
|
||||
return;
|
||||
}
|
||||
let fill = self.options.desktop.to_string().repeat(area.width as usize);
|
||||
for row in 0..area.height {
|
||||
put(
|
||||
@@ -330,11 +334,13 @@ impl App {
|
||||
self.hits.push((r, Hit::ProjectMember(row)));
|
||||
}
|
||||
}
|
||||
WindowKind::Output => {
|
||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), inner)
|
||||
}
|
||||
kind => put(
|
||||
f,
|
||||
inner,
|
||||
match kind {
|
||||
WindowKind::Output => "Output · Ausführung folgt in Change 04",
|
||||
WindowKind::Help => "Help · Inhalte folgen in Change 07",
|
||||
_ => "Debugger-Inhalte folgen in Change 06",
|
||||
},
|
||||
|
||||
@@ -1,109 +1 @@
|
||||
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.raw)(true)?;
|
||||
execute!(
|
||||
guard.writer,
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
EnableBracketedPaste,
|
||||
Hide
|
||||
)?;
|
||||
Ok(guard)
|
||||
}
|
||||
}
|
||||
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"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
pub use tb_ui::terminal::TerminalGuard;
|
||||
|
||||
Reference in New Issue
Block a user