4runr-os-mk3 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/io/mod.rs ADDED
@@ -0,0 +1,66 @@
1
+ mod protocol;
2
+ mod stdio;
3
+ mod ws;
4
+
5
+ use crate::app::{App, Message};
6
+ use anyhow::Result;
7
+ use serde_json;
8
+ use std::io::Write;
9
+ use std::sync::mpsc;
10
+ use std::thread;
11
+
12
+ pub struct IoHandler {
13
+ receiver: mpsc::Receiver<Message>,
14
+ #[allow(dead_code)]
15
+ sender: mpsc::Sender<String>,
16
+ }
17
+
18
+ impl IoHandler {
19
+ pub fn new() -> Result<Self> {
20
+ let (_tx, rx) = mpsc::channel();
21
+ let (cmd_tx, _cmd_rx) = mpsc::channel();
22
+
23
+ // DO NOT spawn any IO threads that read from stdin!
24
+ // The TUI needs exclusive access to stdin for keyboard input.
25
+ // Future: use WebSocket or named pipe for backend communication.
26
+
27
+ // Command sender thread (writes to stdout, but doesn't block stdin)
28
+ thread::spawn(move || {
29
+ loop {
30
+ match _cmd_rx.recv() {
31
+ Ok(cmd) => {
32
+ let cmd_json = serde_json::json!({
33
+ "type": "command",
34
+ "text": cmd
35
+ });
36
+ if let Ok(json_str) = serde_json::to_string(&cmd_json) {
37
+ println!("{}", json_str);
38
+ let _ = std::io::stdout().flush();
39
+ }
40
+ }
41
+ Err(_) => break,
42
+ }
43
+ }
44
+ });
45
+
46
+ Ok(Self {
47
+ receiver: rx,
48
+ sender: cmd_tx,
49
+ })
50
+ }
51
+
52
+ pub fn update(&mut self, app: &mut App) -> Result<()> {
53
+ // Non-blocking receive
54
+ while let Ok(message) = self.receiver.try_recv() {
55
+ app.update(message);
56
+ }
57
+ Ok(())
58
+ }
59
+
60
+ #[allow(dead_code)]
61
+ pub fn send_command(&mut self, cmd: String) -> Result<()> {
62
+ self.sender.send(cmd)?;
63
+ Ok(())
64
+ }
65
+ }
66
+
@@ -0,0 +1,15 @@
1
+ use crate::app::Message;
2
+ use anyhow::Result;
3
+ use std::sync::mpsc;
4
+
5
+ #[allow(dead_code)]
6
+ pub trait Protocol: Send {
7
+ fn run(&mut self) -> Result<()>;
8
+ }
9
+
10
+ #[allow(dead_code)]
11
+ pub struct ProtocolContext {
12
+ pub message_tx: mpsc::Sender<Message>,
13
+ pub command_rx: mpsc::Receiver<String>,
14
+ }
15
+
@@ -0,0 +1,32 @@
1
+ use crate::app::Message;
2
+ use crate::io::protocol::Protocol;
3
+ use anyhow::Result;
4
+ use std::sync::mpsc;
5
+
6
+ #[allow(dead_code)]
7
+ pub struct StdioProtocol {
8
+ #[allow(dead_code)]
9
+ message_tx: mpsc::Sender<Message>,
10
+ }
11
+
12
+ impl StdioProtocol {
13
+ #[allow(dead_code)]
14
+ pub fn new(message_tx: mpsc::Sender<Message>) -> Result<Self> {
15
+ Ok(Self { message_tx })
16
+ }
17
+ }
18
+
19
+ impl Protocol for StdioProtocol {
20
+ fn run(&mut self) -> Result<()> {
21
+ // DO NOT read from stdin here!
22
+ // The TUI needs stdin for keyboard input via crossterm.
23
+ // This protocol is a no-op for now.
24
+ // In the future, we can use a named pipe or socket for IPC.
25
+
26
+ // Just sleep forever to keep the thread alive
27
+ loop {
28
+ std::thread::sleep(std::time::Duration::from_secs(3600));
29
+ }
30
+ }
31
+ }
32
+
package/src/io/ws.rs ADDED
@@ -0,0 +1,32 @@
1
+ use crate::app::Message;
2
+ use crate::io::protocol::Protocol;
3
+ use anyhow::Result;
4
+ use std::sync::mpsc;
5
+
6
+ #[allow(dead_code)]
7
+ pub struct WsProtocol {
8
+ url: String,
9
+ message_tx: mpsc::Sender<Message>,
10
+ }
11
+
12
+ impl WsProtocol {
13
+ #[allow(dead_code)]
14
+ pub fn new(
15
+ url: String,
16
+ message_tx: mpsc::Sender<Message>,
17
+ ) -> Result<Self> {
18
+ Ok(Self {
19
+ url,
20
+ message_tx,
21
+ })
22
+ }
23
+ }
24
+
25
+ impl Protocol for WsProtocol {
26
+ fn run(&mut self) -> Result<()> {
27
+ // This will be implemented with tokio runtime
28
+ // For now, return error to fallback to stdio
29
+ anyhow::bail!("WebSocket not yet implemented, use stdio");
30
+ }
31
+ }
32
+
package/src/main.rs ADDED
@@ -0,0 +1,119 @@
1
+ use anyhow::Result;
2
+ use crossterm::cursor;
3
+ use crossterm::event::{self, Event, KeyEventKind};
4
+ use crossterm::execute;
5
+ use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, Clear, ClearType};
6
+ use ratatui::prelude::*;
7
+ use std::time::{Duration, Instant};
8
+
9
+ mod app;
10
+ mod io;
11
+ mod ui;
12
+
13
+ use app::App;
14
+ use io::IoHandler;
15
+
16
+ fn main() -> Result<()> {
17
+ // Setup terminal (boot screen is now part of TUI)
18
+ enable_raw_mode()?;
19
+ let mut stdout = std::io::stdout();
20
+
21
+ // Enter alternate screen, hide cursor, clear screen, move to top-left
22
+ execute!(
23
+ stdout,
24
+ EnterAlternateScreen,
25
+ cursor::Hide,
26
+ Clear(ClearType::All),
27
+ cursor::MoveTo(0, 0)
28
+ )?;
29
+
30
+ let backend = CrosstermBackend::new(stdout);
31
+ let mut terminal = Terminal::new(backend)?;
32
+
33
+ // Clear the terminal buffer to ensure no artifacts
34
+ terminal.clear()?;
35
+
36
+ // Create app
37
+ let mut app = App::new();
38
+ let mut io_handler = IoHandler::new()?;
39
+
40
+ // Track real time for uptime (not from tick counter)
41
+ let start_time = Instant::now();
42
+ let mut last_tick = Instant::now();
43
+
44
+ // Force initial render
45
+ app.request_render("initial");
46
+
47
+ // Main loop - CRITICAL FIX: Animations run EVERY iteration, not just when no input!
48
+ // Per instructions Bug 1 fix: Move tick() and uptime OUTSIDE poll block
49
+ loop {
50
+ // ┌─────────────────────────────────────────────────────────┐
51
+ // │ STEP 1: UPDATE ANIMATIONS (runs EVERY loop iteration) │
52
+ // └─────────────────────────────────────────────────────────┘
53
+
54
+ // Uptime - always update from real clock (OUTSIDE poll block)
55
+ app.state.uptime_secs = start_time.elapsed().as_secs();
56
+
57
+ // Spinner - update on timer (every 150ms) - OUTSIDE poll block
58
+ if last_tick.elapsed() >= Duration::from_millis(150) {
59
+ app.tick(); // Advances spinner_frame
60
+ last_tick = Instant::now();
61
+ }
62
+
63
+ // ┌─────────────────────────────────────────────────────────┐
64
+ // │ STEP 2: CHECK RENDER SCHEDULER │
65
+ // └─────────────────────────────────────────────────────────┘
66
+ // Check if scheduler says we should render
67
+ if app.should_render() {
68
+ // Scheduler requested render
69
+ }
70
+
71
+ // Check input debounce
72
+ if let Some(debounce_time) = app.input_debounce {
73
+ let elapsed = debounce_time.elapsed();
74
+ if elapsed >= app.input_debounce_duration {
75
+ app.input_debounce = None;
76
+ app.request_render("input_debounce");
77
+ }
78
+ }
79
+
80
+ // ┌─────────────────────────────────────────────────────────┐
81
+ // │ STEP 3: RENDER (runs EVERY loop iteration) │
82
+ // └─────────────────────────────────────────────────────────┘
83
+ // Always render to show animation updates (animations run every loop)
84
+ let render_start = Instant::now();
85
+ terminal.draw(|f| app.render(f))?;
86
+ let render_duration = render_start.elapsed().as_millis() as u64;
87
+ app.record_render(render_duration);
88
+
89
+ // ┌─────────────────────────────────────────────────────────┐
90
+ // │ STEP 4: CHECK FOR INPUT (non-blocking with timeout) │
91
+ // └─────────────────────────────────────────────────────────┘
92
+ // poll() with short timeout (16ms) - returns immediately if no input
93
+ // This allows loop to run ~60 times/second for smooth animations
94
+ if crossterm::event::poll(Duration::from_millis(16))? {
95
+ if let Event::Key(key) = event::read()? {
96
+ if key.kind == KeyEventKind::Press {
97
+ if app.handle_input(key, &mut io_handler)? {
98
+ break; // Exit requested
99
+ }
100
+ }
101
+ }
102
+ }
103
+ // If no input, loop continues immediately - animations keep running!
104
+
105
+ // Check for IO updates (non-blocking)
106
+ io_handler.update(&mut app).ok();
107
+ }
108
+
109
+ // Restore terminal
110
+ disable_raw_mode()?;
111
+ execute!(
112
+ terminal.backend_mut(),
113
+ cursor::Show,
114
+ LeaveAlternateScreen
115
+ )?;
116
+
117
+ Ok(())
118
+ }
119
+
package/src/ui/boot.rs ADDED
@@ -0,0 +1,150 @@
1
+ use crate::app::AppState;
2
+ use ratatui::prelude::*;
3
+ use ratatui::widgets::{Block, Borders, Gauge, Paragraph};
4
+
5
+ // Color constants matching the TUI palette (exact match from layout.rs)
6
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
7
+ #[allow(dead_code)]
8
+ const BRAND_VIOLET: Color = Color::Rgb(148, 103, 189);
9
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
10
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
11
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
12
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
13
+ #[allow(dead_code)]
14
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
15
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
16
+
17
+ /// Exact logo from requirements (monospace, preserve spacing)
18
+ const LOGO: &str = r#"██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██████╗ ██████╗ ███████╗
19
+ ██║ ██║██╔══██╗██║ ██║████╗ ██║██╔══██╗ ██╔═══██╗██╔════╝
20
+ ███████║██████╔╝██║ ██║██╔██╗ ██║██████╔╝ ██║ ██║███████╗
21
+ ╚════██║██╔══██╗██║ ██║██║╚██╗██║██╔══██╗ ██║ ██║╚════██║
22
+ ██║██║ ██║╚██████╔╝██║ ╚████║██║ ██║ ╚██████╔╝███████║
23
+ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝"#;
24
+
25
+ pub fn render_boot(f: &mut Frame, area: Rect, state: &AppState) {
26
+ // Clear background
27
+ f.render_widget(
28
+ Block::default().style(Style::default().bg(BG_PANEL)),
29
+ area,
30
+ );
31
+
32
+ // Calculate layout: logo on top half, boot logs below, progress bar, footer
33
+ let logo_height: u16 = 6; // Logo is 6 lines
34
+ let boot_logs_height: u16 = state.boot_lines.len().min(10) as u16; // Max 10 lines visible
35
+ let progress_height: u16 = 3;
36
+ let footer_height: u16 = 1;
37
+
38
+ let total_content_height: u16 = logo_height + 2 + boot_logs_height + 2 + progress_height + 2 + footer_height;
39
+ let start_y: u16 = (area.height.saturating_sub(total_content_height)) / 2;
40
+
41
+ // === LOGO (centered horizontally and vertically) ===
42
+ let logo_lines: Vec<&str> = LOGO.lines().collect();
43
+ let logo_width: u16 = logo_lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
44
+ // Center horizontally: (area.width - logo_width) / 2
45
+ let logo_x: u16 = area.x + (area.width.saturating_sub(logo_width)) / 2;
46
+
47
+ for (i, line) in logo_lines.iter().enumerate() {
48
+ let line_y = start_y + i as u16;
49
+ if line_y < area.height {
50
+ // Center each line within the available width
51
+ let line_rect = Rect {
52
+ x: logo_x,
53
+ y: line_y,
54
+ width: logo_width.min(area.width.saturating_sub(logo_x)),
55
+ height: 1,
56
+ };
57
+ f.render_widget(
58
+ Paragraph::new(*line)
59
+ .style(Style::default().fg(BRAND_PURPLE))
60
+ .alignment(Alignment::Center), // Center alignment for perfect centering
61
+ line_rect,
62
+ );
63
+ }
64
+ }
65
+
66
+ let current_y = start_y + logo_height + 2;
67
+
68
+ // === BOOT LOGS ===
69
+ if !state.boot_lines.is_empty() && current_y < area.height {
70
+ // Center logs horizontally
71
+ let logs_width: u16 = (area.width * 60 / 100).min(area.width.saturating_sub(40)); // 60% width, safe margins
72
+ let logs_x: u16 = area.x + (area.width.saturating_sub(logs_width)) / 2;
73
+
74
+ let logs_area = Rect {
75
+ x: logs_x,
76
+ y: current_y,
77
+ width: logs_width,
78
+ height: boot_logs_height,
79
+ };
80
+
81
+ let mut log_text = String::new();
82
+ for line in state.boot_lines.iter() {
83
+ log_text.push_str(line);
84
+ log_text.push('\n');
85
+ }
86
+
87
+ f.render_widget(
88
+ Paragraph::new(log_text.trim())
89
+ .style(Style::default().fg(TEXT_PRIMARY))
90
+ .alignment(Alignment::Center), // Center logs too
91
+ logs_area,
92
+ );
93
+ }
94
+
95
+ let current_y = current_y + boot_logs_height + 2;
96
+
97
+ // === PROGRESS BAR ===
98
+ if current_y < area.height {
99
+ // Center progress bar horizontally
100
+ let progress_width: u16 = (area.width * 60 / 100).min(area.width.saturating_sub(40));
101
+ let progress_x: u16 = area.x + (area.width.saturating_sub(progress_width)) / 2;
102
+
103
+ let progress_area = Rect {
104
+ x: progress_x,
105
+ y: current_y,
106
+ width: progress_width,
107
+ height: 3,
108
+ };
109
+
110
+ let progress_label = if state.boot_done {
111
+ "System Ready"
112
+ } else {
113
+ "Booting..."
114
+ };
115
+
116
+ f.render_widget(
117
+ Gauge::default()
118
+ .block(
119
+ Block::default()
120
+ .borders(Borders::NONE)
121
+ .title(progress_label)
122
+ .title_style(Style::default().fg(CYBER_CYAN))
123
+ )
124
+ .gauge_style(Style::default().fg(NEON_GREEN))
125
+ .percent(state.boot_progress as u16)
126
+ .label(format!("{}%", state.boot_progress)),
127
+ progress_area,
128
+ );
129
+ }
130
+
131
+ let current_y = current_y + progress_height + 2;
132
+
133
+ // === FOOTER: "Press any key to continue" (only when boot done) ===
134
+ if state.boot_done && current_y < area.height {
135
+ let footer_area = Rect {
136
+ x: area.x,
137
+ y: current_y,
138
+ width: area.width,
139
+ height: footer_height,
140
+ };
141
+
142
+ f.render_widget(
143
+ Paragraph::new("Press any key to continue...")
144
+ .style(Style::default().fg(TEXT_DIM))
145
+ .alignment(Alignment::Center),
146
+ footer_area,
147
+ );
148
+ }
149
+ }
150
+