implements PLN-0062 native shell lifecycle wiring

This commit is contained in:
bQUARKz 2026-05-15 17:53:10 +01:00
parent f62be06dc5
commit edfac5a99b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 156 additions and 55 deletions

View File

@ -171,6 +171,7 @@ impl Firmware {
#[cfg(test)]
mod tests {
use super::*;
use crate::firmware::firmware_state::HubHomeStep;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
@ -281,6 +282,14 @@ mod tests {
(firmware, hardware, signals, task_id)
}
fn hub_home_firmware() -> (Firmware, Hardware) {
let mut firmware = Firmware::new(None);
firmware.state = FirmwareState::HubHome(HubHomeStep);
firmware.state_initialized = false;
(firmware, Hardware::new())
}
fn focus_window(firmware: &mut Firmware, owner: WindowOwner) {
let mut windows = firmware.os.windows();
windows.remove_all_windows();
@ -406,6 +415,65 @@ mod tests {
assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1);
}
#[test]
fn hub_home_launches_shell_a_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
let signals = InputSignals { a_signal: true, ..Default::default() };
firmware.tick(&signals, &mut hardware);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
assert!(firmware.os.windows().focused_window_belongs_to_task(task_id));
assert_eq!(firmware.os.windows().window_count(), 1);
assert_eq!(firmware.os.windows().windows()[0].title, "ShellA");
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(task_id),
Ok(ProcessState::Running)
);
}
#[test]
fn hub_home_launches_shell_b_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
let signals = InputSignals { b_signal: true, ..Default::default() };
firmware.tick(&signals, &mut hardware);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
assert!(firmware.os.windows().focused_window_belongs_to_task(task_id));
assert_eq!(firmware.os.windows().window_count(), 1);
assert_eq!(firmware.os.windows().windows()[0].title, "ShellB");
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Foreground));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(task_id),
Ok(ProcessState::Running)
);
}
#[test]
fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() {
let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware();
let close_signals = InputSignals { start_signal: true, ..Default::default() };
firmware.tick(&close_signals, &mut hardware);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(task_id),
Ok(ProcessState::Stopped)
);
}
#[test]
fn shell_running_closes_task_and_returns_hub_home_without_focused_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();

View File

@ -1,6 +1,9 @@
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, ShellRunningStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;
use prometeu_system::SystemProfileAction;
use prometeu_system::windows::WindowOwner;
#[derive(Debug, Clone)]
pub struct HubHomeStep;
@ -16,6 +19,18 @@ impl HubHomeStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
if let Some(SystemProfileAction::LaunchNativeShell(app)) = outcome.action {
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
let id = ctx.os.windows().add_window(
app.title().to_string(),
WindowOwner::Task(task_id),
Rect { x: 40, y: 20, w: 400, h: 220 },
app.color(),
);
ctx.os.windows().set_focus(id);
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
}
None
}

View File

@ -1,8 +1,8 @@
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::LogSource;
use prometeu_system::CrashReport;
use prometeu_system::task::{TaskId, TaskState};
use prometeu_system::{CrashReport, SystemProfileAction};
#[derive(Debug, Clone)]
pub struct ShellRunningStep {
@ -66,6 +66,16 @@ impl ShellRunningStep {
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
}
if outcome.action == Some(SystemProfileAction::CloseShell) {
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
ctx.os.error(
LogSource::Hub,
format!("Failed to close shell task {:?}: {:?}", self.task_id, error),
);
}
return Some(FirmwareState::HubHome(HubHomeStep));
}
if !ctx.os.windows().focused_window_belongs_to_task(self.task_id) {
ctx.os.info(
LogSource::Hub,

View File

@ -5,7 +5,7 @@ mod services;
pub use crash_report::CrashReport;
pub use os::{LifecycleError, LifecycleOperation, SystemOS};
pub use programs::PrometeuHub;
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
pub use services::fs;
pub use services::process;
pub use services::task;

View File

@ -1,3 +1,3 @@
mod prometeu_hub;
pub use prometeu_hub::PrometeuHub;
pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};

View File

@ -1,4 +1,4 @@
#[allow(clippy::module_inception)]
mod prometeu_hub;
pub use prometeu_hub::PrometeuHub;
pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};

View File

@ -1,16 +1,49 @@
use crate::windows::WindowOwner;
use crate::{CrashReport, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_vm::VirtualMachine;
/// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeShellApp {
ShellA,
ShellB,
}
impl NativeShellApp {
pub fn app_id(self) -> u32 {
match self {
Self::ShellA => 10_001,
Self::ShellB => 10_002,
}
}
pub fn title(self) -> &'static str {
match self {
Self::ShellA => "ShellA",
Self::ShellB => "ShellB",
}
}
pub fn color(self) -> Color {
match self {
Self::ShellA => Color::GREEN,
Self::ShellB => Color::BLUE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemProfileAction {
LaunchNativeShell(NativeShellApp),
CloseShell,
}
pub struct SystemProfileUpdate {
pub crash: Option<CrashReport>,
pub action: Option<SystemProfileAction>,
}
impl Default for PrometeuHub {
@ -28,43 +61,19 @@ impl PrometeuHub {
// Initializes the Window System and lists apps
}
pub fn gui_update(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) {
pub fn gui_update(
&mut self,
_os: &mut SystemOS,
hw: &mut dyn HardwareBridge,
) -> Option<SystemProfileAction> {
hw.gfx_mut().clear(Color::BLACK);
let mut next_window = None;
if hw.pad().a().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
next_window = Some((
"Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN,
));
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
} else if hw.pad().b().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
next_window = Some((
"Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO,
));
} else if hw.pad().x().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
next_window = Some((
"Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW,
));
} else if hw.pad().y().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
}
if let Some((title, rect, color)) = next_window {
let mut window = os.windows();
window.remove_all_windows();
let id = window.add_window(title, WindowOwner::Hub, rect, color);
window.set_focus(id);
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
} else {
None
}
}
@ -88,12 +97,11 @@ impl PrometeuHub {
hw: &mut dyn HardwareBridge,
) -> SystemProfileUpdate {
let mut crash = None;
let mut action = self.gui_update(os, hw);
self.gui_update(os, hw);
if let Some(focused_id) = os.windows().focused_window() {
if os.windows().focused_window().is_some() {
if hw.pad().start().down {
os.windows().close_window(focused_id);
action = Some(SystemProfileAction::CloseShell);
} else {
crash = os.vm().tick(vm, signals, hw);
}
@ -102,6 +110,6 @@ impl PrometeuHub {
self.render(os, hw);
hw.gfx_mut().present();
SystemProfileUpdate { crash }
SystemProfileUpdate { crash, action }
}
}

View File

@ -34,4 +34,4 @@
{"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]}
{"type":"discussion","id":"DSC-0032","status":"done","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0032-system-os-lifecycle-process-task-contract/LSN-0041-systemos-lifecycle-authority.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0033","status":"done","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0033-system-os-service-ownership-and-module-layout/LSN-0042-systemos-service-ownership-boundary.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-prometeu-hub-ui-direction.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0028","file":"DEC-0028-prometeu-hub-initial-retro-shell-ui-slice.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-native-shell-launch-and-lifecycle-wiring.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0063","file":"PLN-0063-hub-mouse-input-and-click-routing.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0064","file":"PLN-0064-retro-hub-home-and-fake-shell-ui.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0065","file":"PLN-0065-pixel-operator-font-integration.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0066","file":"PLN-0066-hub-ui-slice-validation-and-lesson.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-prometeu-hub-ui-direction.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0028","file":"DEC-0028-prometeu-hub-initial-retro-shell-ui-slice.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-native-shell-launch-and-lifecycle-wiring.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0063","file":"PLN-0063-hub-mouse-input-and-click-routing.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0064","file":"PLN-0064-retro-hub-home-and-fake-shell-ui.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0065","file":"PLN-0065-pixel-operator-font-integration.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0066","file":"PLN-0066-hub-ui-slice-validation-and-lesson.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]}],"lessons":[]}

View File

@ -2,7 +2,7 @@
id: PLN-0062
ticket: prometeu-hub-ui-direction
title: Native Shell Launch and Lifecycle Wiring
status: open
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
@ -82,13 +82,13 @@ Make the launch/close path architecturally correct before the visual slice depen
## Acceptance Criteria
- [ ] Launching `ShellA` creates a native Shell task/process.
- [ ] Launching `ShellB` creates a native Shell task/process.
- [ ] Each launched Shell app receives a focused `WindowOwner::Task(task_id)` window.
- [ ] Closing the Shell app closes lifecycle state through `os.lifecycle().close_task(task_id)`.
- [ ] Closing the Shell app returns firmware to `HubHome`.
- [ ] Hub visual code does not directly mutate task/process managers.
- [ ] Existing shell liveness behavior remains intact.
- [x] Launching `ShellA` creates a native Shell task/process.
- [x] Launching `ShellB` creates a native Shell task/process.
- [x] Each launched Shell app receives a focused `WindowOwner::Task(task_id)` window.
- [x] Closing the Shell app closes lifecycle state through `os.lifecycle().close_task(task_id)`.
- [x] Closing the Shell app returns firmware to `HubHome`.
- [x] Hub visual code does not directly mutate task/process managers.
- [x] Existing shell liveness behavior remains intact.
## Tests