From acf1f022a0d4b2369925c40bbda60f0ed45b58f1 Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Fri, 13 Feb 2026 15:32:54 -0500 Subject: [PATCH] feat: add btop system monitor tab (F3) to kernel terminal manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a third tab to the kernel terminal manager that displays live system monitoring data: process list, memory usage, uptime, and tracing counters. The monitor auto-refreshes every ~1 second using a tick-based check in the render thread. Key changes: - Terminal manager extended from 2 tabs to 3 (Shell/Logs/Monitor) - F3 key and mouse click support for Monitor tab switching - render_btop_view() collects process data under try_lock, releases before formatting to avoid single-CPU ARM64 deadlocks - Render thread checks get_ticks() directly for refresh timing, avoiding feature-flag dependencies on timer ISR - Fix fork state transition: both ARM64 and x86_64 fork paths now call set_main_thread() to properly transition Creating → Ready - Revert init.rs to spawn bsh directly (not bwm) - Revert sys_fbmmap to always use left-pane-only mapping - Add lowercase a-z and symbol support to bitmap font - Add kernel log ring buffer, procfs additions, cpu_ticks field - Add standalone userspace btop.rs and bwm.rs programs - Add PTY infrastructure and libbreenix graphics wrappers Co-Authored-By: Ryan Breen Co-Authored-By: Claude Opus 4.6 --- kernel/src/arch_impl/aarch64/syscall_entry.rs | 2 + kernel/src/fs/procfs/mod.rs | 40 +- kernel/src/graphics/render_queue.rs | 11 + kernel/src/graphics/render_task.rs | 3 + kernel/src/graphics/terminal_manager.rs | 371 +++++- kernel/src/lib.rs | 2 + kernel/src/log_buffer.rs | 95 ++ kernel/src/process/manager.rs | 13 +- kernel/src/process/process.rs | 4 + kernel/src/serial.rs | 1 + kernel/src/syscall/clone.rs | 1 + kernel/src/syscall/dispatcher.rs | 2 + kernel/src/syscall/graphics.rs | 1 + kernel/src/syscall/handler.rs | 2 + kernel/src/syscall/handlers.rs | 11 + kernel/src/syscall/mod.rs | 3 + kernel/src/task/scheduler.rs | 2 + kernel/src/task/thread.rs | 12 + kernel/src/time/timer.rs | 1 + libs/libbreenix/src/graphics.rs | 9 + libs/libbreenix/src/syscall.rs | 2 + libs/libgfx/src/font.rs | 51 + userspace/programs/Cargo.toml | 8 + userspace/programs/build.sh | 4 + userspace/programs/src/btop.rs | 558 +++++++++ userspace/programs/src/bwm.rs | 1035 +++++++++++++++++ userspace/programs/src/init.rs | 6 +- 27 files changed, 2205 insertions(+), 45 deletions(-) create mode 100644 kernel/src/log_buffer.rs create mode 100644 userspace/programs/src/btop.rs create mode 100644 userspace/programs/src/bwm.rs diff --git a/kernel/src/arch_impl/aarch64/syscall_entry.rs b/kernel/src/arch_impl/aarch64/syscall_entry.rs index 67477e90..acd94329 100644 --- a/kernel/src/arch_impl/aarch64/syscall_entry.rs +++ b/kernel/src/arch_impl/aarch64/syscall_entry.rs @@ -492,6 +492,8 @@ fn dispatch_syscall( SyscallNumber::AudioInit => result_to_u64(crate::syscall::audio::sys_audio_init()), SyscallNumber::AudioWrite => result_to_u64(crate::syscall::audio::sys_audio_write(arg1, arg2)), + // Display takeover + SyscallNumber::TakeOverDisplay => result_to_u64(crate::syscall::handlers::sys_take_over_display()), // Testing/diagnostic syscalls SyscallNumber::CowStats => sys_cow_stats_aarch64(arg1), SyscallNumber::SimulateOom => sys_simulate_oom_aarch64(arg1), diff --git a/kernel/src/fs/procfs/mod.rs b/kernel/src/fs/procfs/mod.rs index 468a0540..a776ff7d 100644 --- a/kernel/src/fs/procfs/mod.rs +++ b/kernel/src/fs/procfs/mod.rs @@ -78,6 +78,10 @@ pub enum ProcEntryType { BreenixDir, /// /proc/breenix/testing - whether testing mode is active BreenixTesting, + /// /proc/pids - list of all process IDs + Pids, + /// /proc/kmsg - kernel log messages + Kmsg, /// /proc/[pid] - per-process directory (dynamic, not registered) PidDir(u64), /// /proc/[pid]/status - per-process status (dynamic, not registered) @@ -107,6 +111,8 @@ impl ProcEntryType { ProcEntryType::Mounts => "mounts", ProcEntryType::BreenixDir => "breenix", ProcEntryType::BreenixTesting => "testing", + ProcEntryType::Pids => "pids", + ProcEntryType::Kmsg => "kmsg", ProcEntryType::PidDir(_) => "pid", ProcEntryType::PidStatus(_) => "status", } @@ -134,6 +140,8 @@ impl ProcEntryType { ProcEntryType::Mounts => "/proc/mounts", ProcEntryType::BreenixDir => "/proc/breenix", ProcEntryType::BreenixTesting => "/proc/breenix/testing", + ProcEntryType::Pids => "/proc/pids", + ProcEntryType::Kmsg => "/proc/kmsg", // Dynamic entries don't have static paths ProcEntryType::PidDir(_) => "/proc/", ProcEntryType::PidStatus(_) => "/proc//status", @@ -163,6 +171,8 @@ impl ProcEntryType { ProcEntryType::Mounts => 8, ProcEntryType::BreenixDir => 200, ProcEntryType::BreenixTesting => 201, + ProcEntryType::Pids => 9, + ProcEntryType::Kmsg => 10, ProcEntryType::PidDir(pid) => 10000 + pid, ProcEntryType::PidStatus(pid) => 20000 + pid, } @@ -232,6 +242,9 @@ pub fn init() { procfs.entries.push(ProcEntry::new(ProcEntryType::BreenixDir)); procfs.entries.push(ProcEntry::new(ProcEntryType::BreenixTesting)); + procfs.entries.push(ProcEntry::new(ProcEntryType::Pids)); + procfs.entries.push(ProcEntry::new(ProcEntryType::Kmsg)); + // Register /proc/trace directory and entries procfs.entries.push(ProcEntry::new(ProcEntryType::TraceDir)); procfs.entries.push(ProcEntry::new(ProcEntryType::TraceEnable)); @@ -401,6 +414,8 @@ pub fn read_entry(entry_type: ProcEntryType) -> Result { ProcEntryType::SlabInfo => Ok(generate_slabinfo()), ProcEntryType::Stat => Ok(generate_stat()), ProcEntryType::CowInfo => Ok(generate_cowinfo()), + ProcEntryType::Pids => Ok(generate_pids()), + ProcEntryType::Kmsg => Ok(generate_kmsg()), ProcEntryType::Mounts => Ok(generate_mounts()), ProcEntryType::BreenixDir => { // Directory listing @@ -732,6 +747,27 @@ fn generate_mounts() -> String { out } +/// Generate /proc/pids content (newline-separated PID list) +fn generate_pids() -> String { + use alloc::format; + + let manager_guard = crate::process::manager(); + let mut out = String::new(); + if let Some(ref manager) = *manager_guard { + let mut pids = manager.all_pids(); + pids.sort(); + for pid in pids { + out.push_str(&format!("{}\n", pid.as_u64())); + } + } + out +} + +/// Generate /proc/kmsg content (kernel log ring buffer) +fn generate_kmsg() -> String { + crate::log_buffer::read_all() +} + // ============================================================================= // Dynamic Per-PID Entries // ============================================================================= @@ -878,7 +914,8 @@ fn generate_pid_status(pid: u64) -> String { FdCount:\t{}\n\ VmCode:\t{} kB\n\ VmHeap:\t{} kB\n\ - VmStack:\t{} kB\n", + VmStack:\t{} kB\n\ + CpuTicks:\t{}\n", process.name, pid, ppid, @@ -888,5 +925,6 @@ fn generate_pid_status(pid: u64) -> String { vm_code_kb, vm_heap_kb, vm_stack_kb, + process.cpu_ticks, ) } diff --git a/kernel/src/graphics/render_queue.rs b/kernel/src/graphics/render_queue.rs index f1f931b8..ff3b7be5 100644 --- a/kernel/src/graphics/render_queue.rs +++ b/kernel/src/graphics/render_queue.rs @@ -174,6 +174,17 @@ pub fn drain_and_render() -> usize { return 0; } + // Skip rendering when userspace has taken over the display + if !crate::graphics::terminal_manager::is_display_active() { + // Still consume the data to prevent buffer backup + let head = QUEUE_HEAD.load(Ordering::Acquire); + let tail = QUEUE_TAIL.load(Ordering::Acquire); + if head != tail { + QUEUE_HEAD.store(tail, Ordering::Release); + } + return 0; + } + // Read current indices let head = QUEUE_HEAD.load(Ordering::Acquire); let tail = QUEUE_TAIL.load(Ordering::Acquire); diff --git a/kernel/src/graphics/render_task.rs b/kernel/src/graphics/render_task.rs index eec1f32b..42492181 100644 --- a/kernel/src/graphics/render_task.rs +++ b/kernel/src/graphics/render_task.rs @@ -79,6 +79,9 @@ fn render_thread_main_kthread() { // Drain captured serial output to the Logs terminal drain_log_capture(); + // Refresh btop monitor view if flag is set and Monitor tab is active + crate::graphics::terminal_manager::refresh_btop_if_needed(); + // Update mouse cursor position from tablet input device #[cfg(target_arch = "aarch64")] update_mouse_cursor(); diff --git a/kernel/src/graphics/terminal_manager.rs b/kernel/src/graphics/terminal_manager.rs index ca5ea07d..fe8af399 100644 --- a/kernel/src/graphics/terminal_manager.rs +++ b/kernel/src/graphics/terminal_manager.rs @@ -2,13 +2,16 @@ //! //! Provides a terminal manager that supports multiple terminal panes //! with keyboard-based navigation and a tabbed header UI. +//! Includes Shell (F1), Logs (F2), and Monitor (F3) tabs. use super::font::Font; use super::primitives::{draw_text, fill_rect, Canvas, Color, Rect, TextStyle}; use super::terminal::TerminalPane; use alloc::collections::VecDeque; +use alloc::format; use alloc::string::String; use alloc::vec::Vec; +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use spin::Mutex; // Architecture-specific framebuffer imports @@ -35,19 +38,23 @@ const SCROLLBAR_TRACK_COLOR: Color = Color::rgb(30, 40, 55); /// Scrollbar thumb color const SCROLLBAR_THUMB_COLOR: Color = Color::rgb(100, 120, 150); +/// Number of tabs +const TAB_COUNT: usize = 3; + /// Terminal identifiers #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TerminalId { Shell = 0, Logs = 1, + Monitor = 2, } impl TerminalId { - #[allow(dead_code)] pub fn from_index(idx: usize) -> Option { match idx { 0 => Some(TerminalId::Shell), 1 => Some(TerminalId::Logs), + 2 => Some(TerminalId::Monitor), _ => None, } } @@ -98,16 +105,20 @@ pub struct TerminalManager { /// Log buffer for the Logs terminal log_buffer: LogBuffer, /// Tab information - tab_titles: [&'static str; 2], - tab_shortcuts: [&'static str; 2], + tab_titles: [&'static str; TAB_COUNT], + tab_shortcuts: [&'static str; TAB_COUNT], /// Unread indicators - has_unread: [bool; 2], + has_unread: [bool; TAB_COUNT], /// Scroll offset for Logs tab (0 = following tail, >0 = scrolled up) logs_scroll_offset: usize, /// Saved pixel data for the shell terminal area (saved when switching away) shell_pixel_backup: Option>, /// Saved cursor position for the shell terminal (col, row) shell_cursor_backup: (usize, usize), + /// Saved pixel data for the monitor terminal area (saved when switching away) + monitor_pixel_backup: Option>, + /// Saved cursor position for the monitor terminal (col, row) + monitor_cursor_backup: (usize, usize), } impl TerminalManager { @@ -137,12 +148,14 @@ impl TerminalManager { cursor_visible: true, font, log_buffer: LogBuffer::new(LOG_BUFFER_SIZE), - tab_titles: ["Shell", "Logs"], - tab_shortcuts: ["F1", "F2"], - has_unread: [false, false], + tab_titles: ["Shell", "Logs", "Monitor"], + tab_shortcuts: ["F1", "F2", "F3"], + has_unread: [false, false, false], logs_scroll_offset: 0, shell_pixel_backup: None, shell_cursor_backup: (0, 0), + monitor_pixel_backup: None, + monitor_cursor_backup: (0, 0), } } @@ -155,17 +168,24 @@ impl TerminalManager { /// Switch to a different terminal. pub fn switch_to(&mut self, id: TerminalId, canvas: &mut impl Canvas) { let new_idx = id as usize; - if new_idx >= 2 || new_idx == self.active_idx { + if new_idx >= TAB_COUNT || new_idx == self.active_idx { return; } // Hide cursor self.terminal_pane.draw_cursor(canvas, false); - // If leaving the Shell tab, save its pixel content and cursor position - if self.active_idx == TerminalId::Shell as usize { - self.shell_cursor_backup = self.terminal_pane.cursor(); - self.shell_pixel_backup = Some(self.save_terminal_pixels(canvas)); + // Save pixel content and cursor for the tab we're leaving + match TerminalId::from_index(self.active_idx) { + Some(TerminalId::Shell) => { + self.shell_cursor_backup = self.terminal_pane.cursor(); + self.shell_pixel_backup = Some(self.save_terminal_pixels(canvas)); + } + Some(TerminalId::Monitor) => { + self.monitor_cursor_backup = self.terminal_pane.cursor(); + self.monitor_pixel_backup = Some(self.save_terminal_pixels(canvas)); + } + _ => {} } self.active_idx = new_idx; @@ -190,6 +210,16 @@ impl TerminalManager { self.logs_scroll_offset = 0; self.render_logs_view(canvas); } + TerminalId::Monitor => { + // Restore saved monitor pixels if available, otherwise render fresh + if let Some(ref saved) = self.monitor_pixel_backup { + self.restore_terminal_pixels(canvas, saved); + let (col, row) = self.monitor_cursor_backup; + self.terminal_pane.set_cursor(col, row); + } else { + self.render_btop_view(canvas); + } + } } self.terminal_pane.draw_cursor(canvas, self.cursor_visible); @@ -274,7 +304,8 @@ impl TerminalManager { // Write shell welcome message self.terminal_pane.write_str(canvas, "Breenix Shell\r\n"); self.terminal_pane.write_str(canvas, "=============\r\n\r\n"); - self.terminal_pane.write_str(canvas, "Press F1 for Shell, F2 for Logs\r\n\r\n"); + self.terminal_pane + .write_str(canvas, "Press F1 for Shell, F2 for Logs, F3 for Monitor\r\n\r\n"); self.terminal_pane.draw_cursor(canvas, true); } @@ -296,7 +327,7 @@ impl TerminalManager { // Draw tabs let mut tab_x = self.region_x + 4; - for idx in 0..2 { + for idx in 0..TAB_COUNT { let is_active = idx == self.active_idx; let title = self.tab_titles[idx]; let shortcut = self.tab_shortcuts[idx]; @@ -348,7 +379,7 @@ impl TerminalManager { .with_color(Color::rgb(120, 140, 160)) .with_font(self.font); - let shortcut_text = alloc::format!("[{}]", shortcut); + let shortcut_text = format!("[{}]", shortcut); draw_text( canvas, (tab_x + TAB_PADDING / 2 + title_width + 4) as i32, @@ -539,7 +570,9 @@ impl TerminalManager { let max_offset = total_lines.saturating_sub(visible_rows); let scroll_range = pane_height.saturating_sub(thumb_height); let thumb_y = if max_offset > 0 { - pane_y + (self.logs_scroll_offset as u64 * scroll_range as u64 / max_offset as u64) as usize + pane_y + + (self.logs_scroll_offset as u64 * scroll_range as u64 / max_offset as u64) + as usize } else { pane_y }; @@ -558,6 +591,177 @@ impl TerminalManager { ); } + /// Render the btop system monitor view. + /// + /// Collects system statistics (process list, memory, uptime, counters) + /// and renders them to the terminal pane. + fn render_btop_view(&mut self, canvas: &mut impl Canvas) { + self.clear_terminal_area(canvas); + + let cols = self.terminal_pane.cols(); + + // --- Header --- + // get_ticks() returns raw tick count at 200 Hz (5ms per tick) + let uptime_ticks = crate::time::timer::get_ticks(); + let uptime_secs = uptime_ticks / 200; // 200 ticks per second + let hours = uptime_secs / 3600; + let minutes = (uptime_secs % 3600) / 60; + let seconds = uptime_secs % 60; + + let header = format!( + "btop - Breenix System Monitor Uptime: {}:{:02}:{:02}", + hours, minutes, seconds + ); + self.terminal_pane.write_str(canvas, &header); + self.terminal_pane.write_str(canvas, "\r\n"); + + // --- Memory --- + let mem_stats = crate::memory::frame_allocator::memory_stats(); + let total_bytes = mem_stats.total_bytes; + let allocated_bytes = + (mem_stats.allocated_frames.saturating_sub(mem_stats.free_list_frames) as u64) * 4096; + let pct = if total_bytes > 0 { + (allocated_bytes * 100 / total_bytes) as usize + } else { + 0 + }; + + // Draw a text-based memory bar + let bar_width = 20usize; + let filled = (pct * bar_width / 100).min(bar_width); + let mut bar = String::with_capacity(bar_width); + for i in 0..bar_width { + if i < filled { + bar.push('#'); + } else { + bar.push('.'); + } + } + + // Use integer formatting to avoid f64 Display (not available in no_std) + let total_mb_int = (total_bytes / (1024 * 1024)) as usize; + let total_mb_frac = ((total_bytes % (1024 * 1024)) * 10 / (1024 * 1024)) as usize; + let used_mb_int = (allocated_bytes / (1024 * 1024)) as usize; + let used_mb_frac = ((allocated_bytes % (1024 * 1024)) * 10 / (1024 * 1024)) as usize; + + let mem_line = format!( + "Memory [{}] {}.{} MB / {}.{} MB ({}%)", + bar, used_mb_int, used_mb_frac, total_mb_int, total_mb_frac, pct + ); + self.terminal_pane.write_str(canvas, "\r\n"); + self.terminal_pane.write_str(canvas, &mem_line); + self.terminal_pane.write_str(canvas, "\r\n"); + + // --- Process list header --- + self.terminal_pane.write_str(canvas, "\r\n"); + self.terminal_pane + .write_str(canvas, " PID PPID STATE CPU% MEM(kB) NAME\r\n"); + + // Separator line sized to terminal width + let sep_len = cols.min(48); + let mut sep = String::with_capacity(sep_len); + for _ in 0..sep_len { + sep.push('-'); + } + self.terminal_pane.write_str(canvas, &sep); + self.terminal_pane.write_str(canvas, "\r\n"); + + // --- Process list --- + // Snapshot process data under the lock, then release it before formatting. + // Holding the process manager lock across format!/rendering can deadlock + // on single-CPU ARM64 if a timer interrupt triggers a context switch that + // needs the same lock. + struct ProcSnapshot { + pid: u64, + ppid: u64, + state: &'static str, + cpu_ticks: u64, + mem_kb: usize, + name: String, + } + let visible_rows = self.terminal_pane.rows(); + let max_procs = visible_rows.saturating_sub(9); + let mut proc_snapshots: Vec = Vec::new(); + let mut total_procs = 0usize; + let mut got_lock = false; + + if let Some(mgr_guard) = crate::process::try_manager() { + if let Some(ref mgr) = *mgr_guard { + let processes = mgr.all_processes(); + total_procs = processes.len(); + for (i, proc) in processes.iter().enumerate() { + if i >= max_procs { + break; + } + proc_snapshots.push(ProcSnapshot { + pid: proc.id.as_u64(), + ppid: proc.parent.map(|p| p.as_u64()).unwrap_or(0), + state: match proc.state { + crate::process::ProcessState::Creating => "Creating", + crate::process::ProcessState::Ready => "Ready", + crate::process::ProcessState::Running => "Running", + crate::process::ProcessState::Blocked => "Blocked", + crate::process::ProcessState::Terminated(_) => "Terminated", + }, + cpu_ticks: proc.cpu_ticks, + mem_kb: (proc.memory_usage.code_size + proc.memory_usage.stack_size) + / 1024, + name: proc.name.clone(), + }); + } + got_lock = true; + } + // mgr_guard dropped here — process manager lock released before formatting + } + + if got_lock { + for snap in &proc_snapshots { + let line = format!( + " {:4} {:5} {:10} {:5} {:6} {}", + snap.pid, snap.ppid, snap.state, snap.cpu_ticks, snap.mem_kb, &snap.name, + ); + self.terminal_pane.write_str(canvas, &line); + self.terminal_pane.write_str(canvas, "\r\n"); + } + if total_procs > proc_snapshots.len() { + let remaining = total_procs - proc_snapshots.len(); + let more_line = format!(" ... {} more processes", remaining); + self.terminal_pane.write_str(canvas, &more_line); + self.terminal_pane.write_str(canvas, "\r\n"); + } + } else { + self.terminal_pane + .write_str(canvas, " (process manager busy)\r\n"); + } + + // --- Counters footer --- + let (syscalls, irqs, ctx_switches, _timer_ticks) = + crate::tracing::providers::counters::get_all_counters(); + let (forks, execs, _cow_faults) = + crate::tracing::providers::counters::get_process_counters(); + + self.terminal_pane.write_str(canvas, "\r\n"); + let counter_line = format!( + "Syscalls: {} | IRQs: {} | Ctx Sw: {}", + syscalls, irqs, ctx_switches + ); + self.terminal_pane.write_str(canvas, &counter_line); + self.terminal_pane.write_str(canvas, "\r\n"); + + let proc_counter_line = format!("Forks: {} | Execs: {}", forks, execs); + self.terminal_pane.write_str(canvas, &proc_counter_line); + self.terminal_pane.write_str(canvas, "\r\n"); + } + + /// Refresh the btop view if the Monitor tab is active. + /// + /// Called from the render thread when the btop refresh flag is set. + pub fn refresh_btop(&mut self, canvas: &mut impl Canvas) { + if self.active_idx == TerminalId::Monitor as usize { + self.render_btop_view(canvas); + } + } + /// Toggle cursor visibility. pub fn toggle_cursor(&mut self, canvas: &mut impl Canvas) { self.cursor_visible = !self.cursor_visible; @@ -571,12 +775,25 @@ impl TerminalManager { } } +/// Whether the terminal manager is active (set to false by take_over_display syscall) +static DISPLAY_ACTIVE: AtomicBool = + AtomicBool::new(true); + /// Global terminal manager pub static TERMINAL_MANAGER: Mutex> = Mutex::new(None); /// Flag to prevent recursive calls -static IN_TERMINAL_CALL: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); +static IN_TERMINAL_CALL: AtomicBool = + AtomicBool::new(false); + +/// Tick count at last btop refresh (used by render thread to throttle refreshes) +static BTOP_LAST_REFRESH_TICK: AtomicU64 = AtomicU64::new(0); + +/// Btop refresh interval in ticks (~1 second). +/// The timer fires at 200 Hz (5ms per tick), so 200 ticks = 1 second. +/// Note: get_monotonic_time() returns raw tick count, not milliseconds, +/// despite the misleading comment in timer.rs. +const BTOP_REFRESH_INTERVAL_TICKS: u64 = 200; /// Initialize the terminal manager. pub fn init_terminal_manager(x: usize, y: usize, width: usize, height: usize) { @@ -593,10 +810,70 @@ pub fn is_terminal_manager_active() -> bool { } } +/// Deactivate the terminal manager (called by take_over_display syscall). +/// After this, all write functions become no-ops and handle_terminal_key returns false. +pub fn deactivate() { + DISPLAY_ACTIVE.store(false, Ordering::SeqCst); + log::info!("Terminal manager deactivated (userspace taking over display)"); +} + +/// Check if the display is still under kernel control. +pub fn is_display_active() -> bool { + DISPLAY_ACTIVE.load(Ordering::Relaxed) +} + +/// Check if enough time has passed and refresh the btop view. +/// +/// Called from the render thread on every iteration. Uses `get_ticks()` to +/// determine if ~1 second has elapsed since the last refresh. This avoids +/// modifying the timer ISR and works on all architectures regardless of +/// feature flags. +pub fn refresh_btop_if_needed() { + if !is_display_active() { + return; + } + + // Check if enough ticks have elapsed (cheap atomic read, no lock) + let now = crate::time::timer::get_ticks(); + let last = BTOP_LAST_REFRESH_TICK.load(Ordering::Relaxed); + if now.wrapping_sub(last) < BTOP_REFRESH_INTERVAL_TICKS { + return; + } + + // Use blocking lock — this runs on the render thread, not in interrupt context + let mut guard = TERMINAL_MANAGER.lock(); + let manager = match guard.as_mut() { + Some(m) => m, + None => return, + }; + + // Only refresh if Monitor tab is active + if manager.active_idx != TerminalId::Monitor as usize { + // Still update the tick so we don't do the lock dance every iteration + BTOP_LAST_REFRESH_TICK.store(now, Ordering::Relaxed); + return; + } + + let fb = match SHELL_FRAMEBUFFER.get() { + Some(f) => f, + None => return, + }; + + let mut fb_guard = fb.lock(); + + manager.refresh_btop(&mut *fb_guard); + + // Update last refresh tick AFTER rendering + BTOP_LAST_REFRESH_TICK.store(now, Ordering::Relaxed); +} + /// Write a character to the shell terminal. pub fn write_char_to_shell(c: char) -> bool { + if !is_display_active() { + return false; + } // Prevent recursive calls - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return false; } @@ -619,14 +896,14 @@ pub fn write_char_to_shell(c: char) -> bool { })() .is_some(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); result } /// Write a string to the shell terminal. #[allow(dead_code)] pub fn write_str_to_shell(s: &str) -> bool { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return false; } @@ -649,7 +926,7 @@ pub fn write_str_to_shell(s: &str) -> bool { })() .is_some(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); result } @@ -659,6 +936,9 @@ pub fn write_str_to_shell(s: &str) -> bool { /// as it acquires locks once for the entire buffer and batches cursor operations. #[allow(dead_code)] pub fn write_bytes_to_shell(bytes: &[u8]) -> bool { + if !is_display_active() { + return false; + } if !write_bytes_to_shell_internal(bytes) { return false; } @@ -688,7 +968,7 @@ pub fn write_bytes_to_shell(bytes: &[u8]) -> bool { /// Internal version for use by render thread which handles its own flushing. /// This avoids double-flushing when the render thread batches work. pub fn write_bytes_to_shell_internal(bytes: &[u8]) -> bool { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return false; } @@ -705,7 +985,7 @@ pub fn write_bytes_to_shell_internal(bytes: &[u8]) -> bool { })() .is_some(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); result } @@ -719,6 +999,9 @@ pub fn write_bytes_to_shell_internal(bytes: &[u8]) -> bool { /// is a separate thread that can safely wait for locks. The IN_TERMINAL_CALL /// guard is for preventing recursion within the same thread. pub fn write_bytes_to_shell_blocking(bytes: &[u8]) -> bool { + if !is_display_active() { + return false; + } // Use blocking locks - this is safe because render thread is not in interrupt context let mut guard = TERMINAL_MANAGER.lock(); let manager = match guard.as_mut() { @@ -741,7 +1024,7 @@ pub fn write_bytes_to_shell_blocking(bytes: &[u8]) -> bool { /// Add a log line to the logs terminal. pub fn write_str_to_logs(s: &str) -> bool { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return false; } @@ -766,7 +1049,7 @@ pub fn write_str_to_logs(s: &str) -> bool { })() .is_some(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); result } @@ -842,7 +1125,10 @@ pub fn handle_logs_arrow_key(keycode: u8) -> bool { /// Toggle cursor in active terminal. pub fn toggle_cursor() { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if !is_display_active() { + return; + } + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return; } @@ -864,12 +1150,12 @@ pub fn toggle_cursor() { Some(()) })(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); } /// Switch to a specific terminal. pub fn switch_terminal(id: TerminalId) { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return; } @@ -893,12 +1179,12 @@ pub fn switch_terminal(id: TerminalId) { Some(()) })(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); } /// Clear the shell terminal. pub fn clear_shell() { - if IN_TERMINAL_CALL.swap(true, core::sync::atomic::Ordering::SeqCst) { + if IN_TERMINAL_CALL.swap(true, Ordering::SeqCst) { return; } @@ -912,7 +1198,9 @@ pub fn clear_shell() { if manager.active_idx == TerminalId::Shell as usize { manager.clear_terminal_area(&mut *fb_guard); manager.draw_tab_bar(&mut *fb_guard); - manager.terminal_pane.draw_cursor(&mut *fb_guard, manager.cursor_visible); + manager + .terminal_pane + .draw_cursor(&mut *fb_guard, manager.cursor_visible); } // Flush framebuffer @@ -925,7 +1213,7 @@ pub fn clear_shell() { Some(()) })(); - IN_TERMINAL_CALL.store(false, core::sync::atomic::Ordering::SeqCst); + IN_TERMINAL_CALL.store(false, Ordering::SeqCst); } /// Handle a mouse click for tab switching. @@ -951,7 +1239,7 @@ pub fn handle_mouse_click(x: usize, y: usize) -> bool { // Hit-test tabs (same layout as draw_tab_bar) let metrics = manager.font.metrics(); let mut tab_x = manager.region_x + 4; - for idx in 0..2 { + for idx in 0..TAB_COUNT { let title = manager.tab_titles[idx]; let shortcut = manager.tab_shortcuts[idx]; let title_width = title.len() * metrics.char_advance(); @@ -962,8 +1250,9 @@ pub fn handle_mouse_click(x: usize, y: usize) -> bool { // Clicked on this tab if idx != manager.active_idx { drop(guard); // Release lock before calling switch_terminal - let id = if idx == 0 { TerminalId::Shell } else { TerminalId::Logs }; - switch_terminal(id); + if let Some(id) = TerminalId::from_index(idx) { + switch_terminal(id); + } return true; } return false; // Already on this tab @@ -978,15 +1267,25 @@ pub fn handle_mouse_click(x: usize, y: usize) -> bool { /// Handle keyboard input for terminal switching. /// Returns true if the key was handled. pub fn handle_terminal_key(scancode: u8) -> bool { + if !is_display_active() { + return false; + } match scancode { 0x3B => { + // F1 switch_terminal(TerminalId::Shell); true } 0x3C => { + // F2 switch_terminal(TerminalId::Logs); true } + 0x3D => { + // F3 + switch_terminal(TerminalId::Monitor); + true + } _ => false, } } diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index caa6d8c8..a8906bbe 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -73,6 +73,8 @@ pub mod boot; pub mod trace; // DTrace-style tracing framework with per-CPU ring buffers pub mod tracing; +// Kernel log ring buffer for /proc/kmsg +pub mod log_buffer; // Parallel boot test framework and BTRT #[cfg(any(feature = "boot_tests", feature = "btrt"))] pub mod test_framework; diff --git a/kernel/src/log_buffer.rs b/kernel/src/log_buffer.rs new file mode 100644 index 00000000..275f5088 --- /dev/null +++ b/kernel/src/log_buffer.rs @@ -0,0 +1,95 @@ +//! Kernel log ring buffer for /proc/kmsg +//! +//! Captures serial output bytes into a lock-free ring buffer so that userspace +//! programs can read kernel logs via /proc/kmsg. +//! +//! On ARM64, this reuses the existing `graphics::log_capture` buffer. +//! On x86_64, this module provides its own capture ring buffer. + +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +/// Size of the log ring buffer (32 KB). +const LOG_BUFFER_SIZE: usize = 32 * 1024; + +/// The log ring buffer. +static mut LOG_BUFFER: [u8; LOG_BUFFER_SIZE] = [0u8; LOG_BUFFER_SIZE]; + +/// Read index (consumer). +static LOG_HEAD: AtomicUsize = AtomicUsize::new(0); + +/// Write index (producer). +static LOG_TAIL: AtomicUsize = AtomicUsize::new(0); + +/// Whether the buffer is initialized. +static LOG_READY: AtomicBool = AtomicBool::new(false); + +/// Initialize the log buffer. +pub fn init() { + LOG_READY.store(true, Ordering::SeqCst); +} + +/// Check if the log buffer is ready. +#[inline] +pub fn is_ready() -> bool { + LOG_READY.load(Ordering::Relaxed) +} + +/// Capture a single byte into the log ring buffer. +/// +/// Called from the serial write path. Must be safe with interrupts disabled. +#[inline] +pub fn capture_byte(byte: u8) { + if !is_ready() { + return; + } + + let tail = LOG_TAIL.load(Ordering::Relaxed); + let next_tail = (tail + 1) % LOG_BUFFER_SIZE; + let head = LOG_HEAD.load(Ordering::Acquire); + + if next_tail == head { + // Buffer full — advance head to drop oldest byte + LOG_HEAD.store((head + 1) % LOG_BUFFER_SIZE, Ordering::Release); + } + + unsafe { + LOG_BUFFER[tail] = byte; + } + LOG_TAIL.store(next_tail, Ordering::Release); +} + +/// Read available log data into a String. +/// +/// This is a non-destructive read that returns all available data +/// but does NOT advance the head pointer, so the same data can be +/// read again (useful for /proc/kmsg which should show full log). +pub fn read_all() -> alloc::string::String { + if !is_ready() { + return alloc::string::String::new(); + } + + let head = LOG_HEAD.load(Ordering::Acquire); + let tail = LOG_TAIL.load(Ordering::Acquire); + + if head == tail { + return alloc::string::String::new(); + } + + let available = if tail > head { + tail - head + } else { + LOG_BUFFER_SIZE - head + tail + }; + + let mut result = alloc::vec::Vec::with_capacity(available); + let mut current = head; + + for _ in 0..available { + unsafe { + result.push(LOG_BUFFER[current]); + } + current = (current + 1) % LOG_BUFFER_SIZE; + } + + alloc::string::String::from_utf8_lossy(&result).into_owned() +} diff --git a/kernel/src/process/manager.rs b/kernel/src/process/manager.rs index 361f2b8f..ca2c86f8 100644 --- a/kernel/src/process/manager.rs +++ b/kernel/src/process/manager.rs @@ -697,6 +697,7 @@ impl ProcessManager { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }; Ok(thread) @@ -769,6 +770,7 @@ impl ProcessManager { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }; Ok(thread) @@ -846,6 +848,7 @@ impl ProcessManager { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }; Ok(thread) @@ -1546,9 +1549,8 @@ impl ProcessManager { child_thread.context.x0 ); - // Set the child process's main thread - child_process.main_thread = Some(child_thread); - + // Set the child process's main thread (also transitions Creating → Ready) + child_process.set_main_thread(child_thread); // Inherit parent's user stack bounds for demand-paged growth if let Some(parent) = self.processes.get(&parent_pid) { @@ -1752,8 +1754,8 @@ impl ProcessManager { child_process.entry_point ); - // Set the child process's main thread - child_process.main_thread = Some(child_thread); + // Set the child process's main thread (also transitions Creating → Ready) + child_process.set_main_thread(child_thread); // CoW fork: no separate stack allocation, so no stack to store child_process.stack = None; @@ -2006,6 +2008,7 @@ impl ProcessManager { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }; // CoW fork: Child uses the same stack virtual addresses as the parent. diff --git a/kernel/src/process/process.rs b/kernel/src/process/process.rs index 7eebaf0b..e0a19a80 100644 --- a/kernel/src/process/process.rs +++ b/kernel/src/process/process.rs @@ -165,6 +165,9 @@ pub struct Process { /// Framebuffer mmap info (if this process has an mmap'd framebuffer) pub fb_mmap: Option, + + /// Accumulated CPU ticks for this process (for btop display) + pub cpu_ticks: u64, } /// Memory usage tracking @@ -216,6 +219,7 @@ impl Process { user_stack_top: 0, pending_old_page_tables: Vec::new(), fb_mmap: None, + cpu_ticks: 0, } } diff --git a/kernel/src/serial.rs b/kernel/src/serial.rs index 848f1b21..7d470870 100644 --- a/kernel/src/serial.rs +++ b/kernel/src/serial.rs @@ -79,6 +79,7 @@ pub fn write_byte(byte: u8) { unsafe { crate::arch_disable_interrupts(); } SERIAL1.lock().send(byte); + crate::log_buffer::capture_byte(byte); // Only re-enable if they were enabled before // This prevents race condition in syscall handler where interrupts must stay disabled diff --git a/kernel/src/syscall/clone.rs b/kernel/src/syscall/clone.rs index d9be96be..18a15da3 100644 --- a/kernel/src/syscall/clone.rs +++ b/kernel/src/syscall/clone.rs @@ -170,6 +170,7 @@ pub fn sys_clone( blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }; // Set has_started to true so we go through the restore path (not first_entry) diff --git a/kernel/src/syscall/dispatcher.rs b/kernel/src/syscall/dispatcher.rs index 4d55f189..da0d617a 100644 --- a/kernel/src/syscall/dispatcher.rs +++ b/kernel/src/syscall/dispatcher.rs @@ -123,6 +123,8 @@ pub fn dispatch_syscall( // Audio syscalls (Breenix-specific) SyscallNumber::AudioInit => super::audio::sys_audio_init(), SyscallNumber::AudioWrite => super::audio::sys_audio_write(arg1, arg2), + // Display takeover (Breenix-specific) + SyscallNumber::TakeOverDisplay => super::handlers::sys_take_over_display(), // Testing/diagnostic syscalls (Breenix-specific) SyscallNumber::CowStats => super::handlers::sys_cow_stats(arg1), SyscallNumber::SimulateOom => super::handlers::sys_simulate_oom(arg1), diff --git a/kernel/src/syscall/graphics.rs b/kernel/src/syscall/graphics.rs index 9008cc1e..99fea0c9 100644 --- a/kernel/src/syscall/graphics.rs +++ b/kernel/src/syscall/graphics.rs @@ -509,6 +509,7 @@ pub fn sys_fbmmap() -> SyscallResult { use crate::memory::arch_stub::{Page, Size4KiB, VirtAddr}; // Get framebuffer dimensions (acquire and release FB lock quickly) + // Always map only the left pane (for demo programs that coexist with the kernel terminal). let (pane_width, height, bpp) = { let fb = match SHELL_FRAMEBUFFER.get() { Some(fb) => fb, diff --git a/kernel/src/syscall/handler.rs b/kernel/src/syscall/handler.rs index 93e225d2..b17413db 100644 --- a/kernel/src/syscall/handler.rs +++ b/kernel/src/syscall/handler.rs @@ -397,6 +397,8 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { // Audio syscalls Some(SyscallNumber::AudioInit) => super::audio::sys_audio_init(), Some(SyscallNumber::AudioWrite) => super::audio::sys_audio_write(args.0, args.1), + // Display takeover + Some(SyscallNumber::TakeOverDisplay) => super::handlers::sys_take_over_display(), None => { log::warn!("Unknown syscall number: {} - returning ENOSYS", syscall_num); SyscallResult::Err(super::ErrorCode::NoSys as u64) diff --git a/kernel/src/syscall/handlers.rs b/kernel/src/syscall/handlers.rs index 8b860c0d..48a19387 100644 --- a/kernel/src/syscall/handlers.rs +++ b/kernel/src/syscall/handlers.rs @@ -3337,6 +3337,17 @@ pub struct CowStatsResult { pub sole_owner_opt: u64, } +/// Take over the display from the kernel terminal manager. +/// After this syscall, the kernel terminal manager is deactivated and +/// the calling process is responsible for rendering to the framebuffer. +pub fn sys_take_over_display() -> SyscallResult { + #[cfg(any(feature = "interactive", target_arch = "aarch64"))] + { + crate::graphics::terminal_manager::deactivate(); + } + SyscallResult::Ok(0) +} + /// sys_cow_stats - Get Copy-on-Write statistics (for testing) /// /// This syscall is used to verify that the CoW optimization paths are working. diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 644c8951..2077379d 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -138,6 +138,8 @@ pub enum SyscallNumber { // Audio syscalls (Breenix-specific) AudioInit = 420, // Breenix: initialize audio stream AudioWrite = 421, // Breenix: write PCM data to audio device + // Display takeover (Breenix-specific) + TakeOverDisplay = 431, // Breenix: userspace takes over display from kernel terminal manager CowStats = 500, // Breenix: get Copy-on-Write statistics (for testing) SimulateOom = 501, // Breenix: enable/disable OOM simulation (for testing) } @@ -235,6 +237,7 @@ impl SyscallNumber { // Audio syscalls 420 => Some(Self::AudioInit), 421 => Some(Self::AudioWrite), + 431 => Some(Self::TakeOverDisplay), 500 => Some(Self::CowStats), 501 => Some(Self::SimulateOom), _ => None, diff --git a/kernel/src/task/scheduler.rs b/kernel/src/task/scheduler.rs index e3df469a..2e3e3741 100644 --- a/kernel/src/task/scheduler.rs +++ b/kernel/src/task/scheduler.rs @@ -490,6 +490,7 @@ impl Scheduler { // Mark new thread as running if let Some(next) = self.get_thread_mut(next_thread_id) { next.set_running(); + next.run_start_ticks = crate::time::get_ticks(); } // Get mutable reference to old thread and immutable to new @@ -615,6 +616,7 @@ impl Scheduler { if let Some(next) = self.get_thread_mut(next_thread_id) { next.set_running(); + next.run_start_ticks = crate::time::get_ticks(); } Some((old_thread_id, next_thread_id, should_requeue_old)) diff --git a/kernel/src/task/thread.rs b/kernel/src/task/thread.rs index 49457e32..f61960d0 100644 --- a/kernel/src/task/thread.rs +++ b/kernel/src/task/thread.rs @@ -402,6 +402,9 @@ pub struct Thread { /// When set, the scheduler will unblock this thread when the monotonic /// clock reaches this value. pub wake_time_ns: Option, + + /// Tick count when this thread started its current run (for CPU accounting) + pub run_start_ticks: u64, } impl Clone for Thread { @@ -424,6 +427,7 @@ impl Clone for Thread { blocked_in_syscall: self.blocked_in_syscall, saved_userspace_context: self.saved_userspace_context.clone(), wake_time_ns: self.wake_time_ns, + run_start_ticks: self.run_start_ticks, } } } @@ -483,6 +487,7 @@ impl Thread { blocked_in_syscall: false, // New thread is not blocked in syscall saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }) } @@ -537,6 +542,7 @@ impl Thread { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, }) } @@ -578,6 +584,7 @@ impl Thread { blocked_in_syscall: false, // New thread is not blocked in syscall saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } @@ -622,6 +629,7 @@ impl Thread { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } @@ -675,6 +683,7 @@ impl Thread { blocked_in_syscall: false, // New thread is not blocked in syscall saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } @@ -723,6 +732,7 @@ impl Thread { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } @@ -796,6 +806,7 @@ impl Thread { blocked_in_syscall: false, // New thread is not blocked in syscall saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } @@ -836,6 +847,7 @@ impl Thread { blocked_in_syscall: false, saved_userspace_context: None, wake_time_ns: None, + run_start_ticks: 0, } } } diff --git a/kernel/src/time/timer.rs b/kernel/src/time/timer.rs index 9b2fb7ef..b73c4d45 100644 --- a/kernel/src/time/timer.rs +++ b/kernel/src/time/timer.rs @@ -71,6 +71,7 @@ pub fn timer_interrupt() { // Toggle cursor - uses try_lock so won't block crate::logger::toggle_cursor_blink(); } + } } diff --git a/libs/libbreenix/src/graphics.rs b/libs/libbreenix/src/graphics.rs index 0ce702f2..6ca0a357 100644 --- a/libs/libbreenix/src/graphics.rs +++ b/libs/libbreenix/src/graphics.rs @@ -374,3 +374,12 @@ impl Framebuffer { // Note: No Drop impl needed. The mmap'd buffer is cleaned up by the kernel // when the process exits. Explicitly munmapping is optional. + +/// Deactivate the kernel's terminal manager so userspace can take over the display. +/// +/// After this call, the kernel will no longer render to the right-side terminal pane. +/// The calling process is responsible for all display rendering via fb_mmap. +pub fn take_over_display() -> Result<(), Error> { + let result = unsafe { raw::syscall0(nr::TAKE_OVER_DISPLAY) }; + Error::from_syscall(result as i64).map(|_| ()) +} diff --git a/libs/libbreenix/src/syscall.rs b/libs/libbreenix/src/syscall.rs index 7d2c7768..6aa468c5 100644 --- a/libs/libbreenix/src/syscall.rs +++ b/libs/libbreenix/src/syscall.rs @@ -98,6 +98,8 @@ pub mod nr { pub const CLONE: u64 = 56; // Linux x86_64 clone pub const FUTEX: u64 = 202; // Linux x86_64 futex pub const GETRANDOM: u64 = 318; // Linux x86_64 getrandom + // Display takeover (Breenix-specific) + pub const TAKE_OVER_DISPLAY: u64 = 431; // Breenix: userspace takes over display // Testing syscalls (Breenix-specific) pub const COW_STATS: u64 = 500; // Breenix: get CoW statistics (for testing) pub const SIMULATE_OOM: u64 = 501; // Breenix: enable/disable OOM simulation (for testing) diff --git a/libs/libgfx/src/font.rs b/libs/libgfx/src/font.rs index 8e9c8882..a4b3d058 100644 --- a/libs/libgfx/src/font.rs +++ b/libs/libgfx/src/font.rs @@ -48,16 +48,67 @@ fn glyph_for(ch: u8) -> [u8; GLYPH_H] { b'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], b'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], b'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], + // Lowercase letters + b'a' => [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F], + b'b' => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1E], + b'c' => [0x00, 0x00, 0x0E, 0x10, 0x10, 0x11, 0x0E], + b'd' => [0x01, 0x01, 0x0D, 0x13, 0x11, 0x11, 0x0F], + b'e' => [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E], + b'f' => [0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08], + b'g' => [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x0E], + b'h' => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11], + b'i' => [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E], + b'j' => [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C], + b'k' => [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12], + b'l' => [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], + b'm' => [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11], + b'n' => [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11], + b'o' => [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E], + b'p' => [0x00, 0x00, 0x1E, 0x11, 0x1E, 0x10, 0x10], + b'q' => [0x00, 0x00, 0x0D, 0x13, 0x0F, 0x01, 0x01], + b'r' => [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10], + b's' => [0x00, 0x00, 0x0E, 0x10, 0x0E, 0x01, 0x1E], + b't' => [0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06], + b'u' => [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D], + b'v' => [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04], + b'w' => [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A], + b'x' => [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11], + b'y' => [0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E], + b'z' => [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F], + // Symbols b':' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00], + b';' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x08], b' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], b'.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C], b',' => [0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08], b'!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04], b'?' => [0x0E, 0x11, 0x01, 0x06, 0x04, 0x00, 0x04], b'-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00], + b'_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F], + b'=' => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00], + b'+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00], + b'*' => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00], b'/' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10], + b'\\' => [0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01], + b'|' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], b'(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02], b')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08], + b'[' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E], + b']' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E], + b'{' => [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02], + b'}' => [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08], + b'<' => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02], + b'>' => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08], + b'#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A], + b'@' => [0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0F], + b'%' => [0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13], + b'^' => [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00], + b'&' => [0x0C, 0x12, 0x0C, 0x0A, 0x11, 0x12, 0x0D], + b'$' => [0x04, 0x0F, 0x14, 0x0E, 0x05, 0x1E, 0x04], + b'~' => [0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00], + b'`' => [0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00], + b'\'' => [0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00], + b'"' => [0x0A, 0x0A, 0x14, 0x00, 0x00, 0x00, 0x00], _ => [0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F], // solid block } } diff --git a/userspace/programs/Cargo.toml b/userspace/programs/Cargo.toml index cfe63b65..bb22ea3d 100644 --- a/userspace/programs/Cargo.toml +++ b/userspace/programs/Cargo.toml @@ -518,6 +518,14 @@ path = "src/tones.rs" name = "bsh" path = "src/bsh.rs" +[[bin]] +name = "bwm" +path = "src/bwm.rs" + +[[bin]] +name = "btop" +path = "src/btop.rs" + [profile.release] panic = "abort" lto = "thin" diff --git a/userspace/programs/build.sh b/userspace/programs/build.sh index 1fe21013..6d4283fa 100755 --- a/userspace/programs/build.sh +++ b/userspace/programs/build.sh @@ -253,6 +253,10 @@ STD_BINARIES=( # Shells "bsh" + # Window manager and system monitor + "bwm" + "btop" + # Services "init" "telnetd" diff --git a/userspace/programs/src/btop.rs b/userspace/programs/src/btop.rs new file mode 100644 index 00000000..1bc39a70 --- /dev/null +++ b/userspace/programs/src/btop.rs @@ -0,0 +1,558 @@ +//! btop - Breenix System Monitor +//! +//! A top/htop-like system monitor that reads from procfs and displays +//! live system statistics. Designed to run inside a PTY via bwm. +//! +//! Reads: +//! /proc/uptime — system uptime +//! /proc/meminfo — memory usage +//! /proc/stat — kernel counters (syscalls, IRQs, context switches, etc.) +//! /proc/pids — list of process IDs +//! /proc//status — per-process info (name, state, memory, CPU ticks) + +use libbreenix::io; +use libbreenix::types::Fd; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/// Read a procfs file into a buffer, return the bytes read +fn read_procfs(path: &str, buf: &mut [u8]) -> usize { + match libbreenix::fs::open(path, 0) { + Ok(fd) => { + let mut total = 0; + loop { + match io::read(fd, &mut buf[total..]) { + Ok(n) if n > 0 => { + total += n; + if total >= buf.len() - 1 { + break; + } + } + _ => break, + } + } + let _ = io::close(fd); + total + } + Err(_) => 0, + } +} + +/// Parse a "key value\n" line from procfs content, return value as u64 +fn parse_value(content: &[u8], key: &[u8]) -> u64 { + // Search for key in content + let mut i = 0; + while i + key.len() < content.len() { + if &content[i..i + key.len()] == key { + // Skip to value (past whitespace) + let mut j = i + key.len(); + while j < content.len() && (content[j] == b' ' || content[j] == b'\t') { + j += 1; + } + // Parse number + let mut val: u64 = 0; + while j < content.len() && content[j] >= b'0' && content[j] <= b'9' { + val = val.saturating_mul(10).saturating_add((content[j] - b'0') as u64); + j += 1; + } + return val; + } + // Skip to next line + while i < content.len() && content[i] != b'\n' { + i += 1; + } + if i < content.len() { + i += 1; + } + } + 0 +} + +/// Parse a "Key:\tValue\n" line from procfs content, return value as string bytes +fn parse_str_value<'a>(content: &'a [u8], key: &[u8]) -> &'a [u8] { + let mut i = 0; + while i + key.len() < content.len() { + if &content[i..i + key.len()] == key { + let mut j = i + key.len(); + while j < content.len() && (content[j] == b' ' || content[j] == b'\t') { + j += 1; + } + let start = j; + while j < content.len() && content[j] != b'\n' { + j += 1; + } + return &content[start..j]; + } + while i < content.len() && content[i] != b'\n' { + i += 1; + } + if i < content.len() { + i += 1; + } + } + b"" +} + +/// Format a number with commas: 12345 -> "12,345" +fn format_number(n: u64, buf: &mut [u8]) -> usize { + if n == 0 { + buf[0] = b'0'; + return 1; + } + + // First, write digits in reverse + let mut tmp = [0u8; 20]; + let mut val = n; + let mut pos = 0; + while val > 0 { + tmp[pos] = b'0' + (val % 10) as u8; + val /= 10; + pos += 1; + } + + // Now write with commas + let mut out = 0; + for i in (0..pos).rev() { + if out > 0 && (pos - 1 - i) > 0 && (pos - 1 - i) % 3 == 0 && i < pos - 1 { + // Actually, let's simplify: insert comma every 3 digits from the right + } + buf[out] = tmp[i]; + out += 1; + } + + // Redo with proper comma insertion + out = 0; + let digits = pos; + for i in 0..digits { + let rev_idx = digits - 1 - i; + buf[out] = tmp[rev_idx]; + out += 1; + // Insert comma after every 3 digits from the right, except at the end + let remaining = rev_idx; + if remaining > 0 && remaining % 3 == 0 { + buf[out] = b','; + out += 1; + } + } + out +} + +/// Write an ANSI string to stdout +fn emit(s: &[u8]) { + let _ = io::write(Fd::from_raw(1), s); +} + +/// Write a string to stdout +fn emit_str(s: &str) { + emit(s.as_bytes()); +} + + +/// Write a u64 number to a buffer, return length +fn write_num(buf: &mut [u8], n: u64) -> usize { + if n == 0 { + buf[0] = b'0'; + return 1; + } + let mut tmp = [0u8; 20]; + let mut val = n; + let mut pos = 0; + while val > 0 { + tmp[pos] = b'0' + (val % 10) as u8; + val /= 10; + pos += 1; + } + for i in 0..pos { + buf[i] = tmp[pos - 1 - i]; + } + pos +} + +/// Print a number to stdout +fn emit_num(n: u64) { + let mut buf = [0u8; 20]; + let len = write_num(&mut buf, n); + emit(&buf[..len]); +} + +/// Print a formatted number (with commas) to stdout +fn emit_formatted_num(n: u64) { + let mut buf = [0u8; 30]; + let len = format_number(n, &mut buf); + emit(&buf[..len]); +} + +/// Process info structure +#[derive(Clone)] +struct ProcInfo { + pid: u64, + ppid: u64, + name: [u8; 32], + name_len: usize, + state: [u8; 16], + state_len: usize, + cpu_ticks: u64, + vm_heap_kb: u64, + vm_stack_kb: u64, + vm_code_kb: u64, +} + +impl ProcInfo { + fn new() -> Self { + Self { + pid: 0, + ppid: 0, + name: [0; 32], + name_len: 0, + state: [0; 16], + state_len: 0, + cpu_ticks: 0, + vm_heap_kb: 0, + vm_stack_kb: 0, + vm_code_kb: 0, + } + } + + fn total_mem_kb(&self) -> u64 { + self.vm_code_kb + self.vm_heap_kb + self.vm_stack_kb + } +} + +/// Parse /proc//status into a ProcInfo +fn parse_proc_status(pid: u64) -> Option { + let mut path_buf = [0u8; 64]; + let prefix = b"/proc/"; + let suffix = b"/status"; + let mut pos = 0; + + for &b in prefix { + path_buf[pos] = b; + pos += 1; + } + let mut tmp = [0u8; 20]; + let nlen = write_num(&mut tmp, pid); + for i in 0..nlen { + path_buf[pos] = tmp[i]; + pos += 1; + } + for &b in suffix { + path_buf[pos] = b; + pos += 1; + } + + let path_str = core::str::from_utf8(&path_buf[..pos]).ok()?; + + let mut buf = [0u8; 512]; + let n = read_procfs(path_str, &mut buf); + if n == 0 { + return None; + } + + let content = &buf[..n]; + let mut info = ProcInfo::new(); + info.pid = pid; + + // Parse Name + let name_val = parse_str_value(content, b"Name:"); + let copy_len = name_val.len().min(31); + info.name[..copy_len].copy_from_slice(&name_val[..copy_len]); + info.name_len = copy_len; + + // Parse PPid + info.ppid = parse_value(content, b"PPid:"); + + // Parse State + let state_val = parse_str_value(content, b"State:"); + let slen = state_val.len().min(15); + info.state[..slen].copy_from_slice(&state_val[..slen]); + info.state_len = slen; + + // Parse CpuTicks + info.cpu_ticks = parse_value(content, b"CpuTicks:"); + + // Parse memory + info.vm_code_kb = parse_value(content, b"VmCode:"); + info.vm_heap_kb = parse_value(content, b"VmHeap:"); + info.vm_stack_kb = parse_value(content, b"VmStack:"); + + Some(info) +} + +/// Parse /proc/pids into a list of PIDs +fn get_pids() -> Vec { + let mut buf = [0u8; 1024]; + let n = read_procfs("/proc/pids", &mut buf); + let content = &buf[..n]; + + let mut pids = Vec::new(); + let mut val: u64 = 0; + let mut in_num = false; + + for &b in content { + if b >= b'0' && b <= b'9' { + val = val * 10 + (b - b'0') as u64; + in_num = true; + } else { + if in_num { + pids.push(val); + val = 0; + in_num = false; + } + } + } + if in_num { + pids.push(val); + } + pids +} + +/// Draw a memory usage bar +fn draw_memory_bar(used_kb: u64, total_kb: u64) { + let bar_width = 30; + let pct = if total_kb > 0 { + ((used_kb * 100) / total_kb) as usize + } else { + 0 + }; + let filled = (pct * bar_width) / 100; + + emit_str("\x1b[1m Memory \x1b[0m["); + for i in 0..bar_width { + if i < filled { + emit_str("\x1b[32m#"); + } else { + emit_str("\x1b[90m."); + } + } + emit_str("\x1b[0m] "); + + // Show used / total + let used_mb_int = used_kb / 1024; + let used_mb_frac = (used_kb % 1024) * 10 / 1024; + let total_mb_int = total_kb / 1024; + let total_mb_frac = (total_kb % 1024) * 10 / 1024; + + emit_num(used_mb_int); + emit_str("."); + emit_num(used_mb_frac); + emit_str(" MB / "); + emit_num(total_mb_int); + emit_str("."); + emit_num(total_mb_frac); + emit_str(" MB ("); + emit_num(pct as u64); + emit_str("%)"); +} + +fn main() { + // Wait a moment for the system to settle + let _ = libbreenix::time::sleep_ms(500); + + // Previous tick counts for CPU% delta computation + let mut prev_ticks: Vec<(u64, u64)> = Vec::new(); // (pid, ticks) + let mut prev_timer_ticks: u64 = 0; + + loop { + // ── Gather Data ────────────────────────────────────────────────── + + // Uptime + let mut uptime_buf = [0u8; 64]; + let uptime_n = read_procfs("/proc/uptime", &mut uptime_buf); + let uptime_secs = parse_value(&uptime_buf[..uptime_n], b""); // First number + + // Actually parse uptime properly - it's "123.45 0.00\n" + let mut up_secs: u64 = 0; + for &b in &uptime_buf[..uptime_n] { + if b >= b'0' && b <= b'9' { + up_secs = up_secs * 10 + (b - b'0') as u64; + } else { + break; + } + } + let _ = uptime_secs; // use up_secs instead + + // Memory + let mut meminfo_buf = [0u8; 1024]; + let meminfo_n = read_procfs("/proc/meminfo", &mut meminfo_buf); + let meminfo = &meminfo_buf[..meminfo_n]; + let total_kb = parse_value(meminfo, b"MemTotal:"); + let free_kb = parse_value(meminfo, b"MemFree:"); + let used_kb = total_kb.saturating_sub(free_kb); + + // Kernel counters + let mut stat_buf = [0u8; 512]; + let stat_n = read_procfs("/proc/stat", &mut stat_buf); + let stat = &stat_buf[..stat_n]; + let syscalls = parse_value(stat, b"syscalls"); + let interrupts = parse_value(stat, b"interrupts"); + let ctx_switches = parse_value(stat, b"context_switches"); + let timer_ticks = parse_value(stat, b"timer_ticks"); + let forks = parse_value(stat, b"forks"); + let execs = parse_value(stat, b"execs"); + let cow_faults = parse_value(stat, b"cow_faults"); + + // Process list + let pids = get_pids(); + let mut procs: Vec = Vec::new(); + for &pid in &pids { + if let Some(info) = parse_proc_status(pid) { + procs.push(info); + } + } + + // Compute CPU% deltas + let tick_delta = timer_ticks.saturating_sub(prev_timer_ticks); + let mut cpu_pcts: Vec<(u64, u64)> = Vec::new(); // (pid, pct*10 for 1 decimal) + for proc in &procs { + let prev = prev_ticks.iter().find(|(p, _)| *p == proc.pid); + let prev_t = prev.map(|(_, t)| *t).unwrap_or(0); + let delta = proc.cpu_ticks.saturating_sub(prev_t); + let pct10 = if tick_delta > 0 { + (delta * 1000) / tick_delta + } else { + 0 + }; + cpu_pcts.push((proc.pid, pct10)); + } + + // Save current ticks for next iteration + prev_ticks.clear(); + for proc in &procs { + prev_ticks.push((proc.pid, proc.cpu_ticks)); + } + prev_timer_ticks = timer_ticks; + + // ── Render ─────────────────────────────────────────────────────── + + // Clear screen and home cursor + emit_str("\x1b[2J\x1b[H"); + + // Header + emit_str("\x1b[1;36mbtop\x1b[0m - Breenix System Monitor"); + + // Uptime (right-aligned) + emit_str(" Uptime: "); + let hours = up_secs / 3600; + let mins = (up_secs % 3600) / 60; + let secs = up_secs % 60; + emit_num(hours); + emit_str(":"); + if mins < 10 { emit_str("0"); } + emit_num(mins); + emit_str(":"); + if secs < 10 { emit_str("0"); } + emit_num(secs); + emit_str("\n\n"); + + // Memory bar + draw_memory_bar(used_kb, total_kb); + emit_str("\n\n"); + + // Process table header + emit_str("\x1b[1m PID PPID STATE CPU% MEM NAME\x1b[0m\n"); + emit_str(" ---- ----- ---------- ------ -------- ----------------\n"); + + // Sort by CPU% descending + // Simple insertion sort since we have few processes + let mut sorted_indices: Vec = (0..procs.len()).collect(); + for i in 1..sorted_indices.len() { + let mut j = i; + while j > 0 { + let a = sorted_indices[j]; + let b = sorted_indices[j - 1]; + let pct_a = cpu_pcts.iter().find(|(p, _)| *p == procs[a].pid).map(|(_, p)| *p).unwrap_or(0); + let pct_b = cpu_pcts.iter().find(|(p, _)| *p == procs[b].pid).map(|(_, p)| *p).unwrap_or(0); + if pct_a > pct_b { + sorted_indices.swap(j, j - 1); + j -= 1; + } else { + break; + } + } + } + + for &idx in &sorted_indices { + let proc = &procs[idx]; + let pct10 = cpu_pcts.iter().find(|(p, _)| *p == proc.pid).map(|(_, p)| *p).unwrap_or(0); + + // Color based on state + let state_bytes = &proc.state[..proc.state_len]; + if state_bytes == b"Running" { + emit_str("\x1b[32m"); // Green + } else if state_bytes == b"Blocked" { + emit_str("\x1b[33m"); // Yellow + } else if state_bytes.starts_with(b"Terminated") { + emit_str("\x1b[31m"); // Red + } + + // PID (right-aligned in 5 chars) + emit_str(" "); + if proc.pid < 10 { emit_str(" "); } + else if proc.pid < 100 { emit_str(" "); } + else if proc.pid < 1000 { emit_str(" "); } + emit_num(proc.pid); + + // PPID + emit_str(" "); + if proc.ppid < 10 { emit_str(" "); } + else if proc.ppid < 100 { emit_str(" "); } + else if proc.ppid < 1000 { emit_str(" "); } + emit_num(proc.ppid); + + // State (padded to 10 chars) + emit_str(" "); + emit(&proc.state[..proc.state_len]); + for _ in proc.state_len..10 { + emit_str(" "); + } + + // CPU% (e.g., " 1.5%") + emit_str(" "); + let pct_int = pct10 / 10; + let pct_frac = pct10 % 10; + if pct_int < 10 { emit_str(" "); } + else if pct_int < 100 { emit_str(" "); } + emit_num(pct_int); + emit_str("."); + emit_num(pct_frac); + emit_str("%"); + + // Memory + emit_str(" "); + let mem = proc.total_mem_kb(); + if mem < 10 { emit_str(" "); } + else if mem < 100 { emit_str(" "); } + else if mem < 1000 { emit_str(" "); } + else if mem < 10000 { emit_str(" "); } + else if mem < 100000 { emit_str(" "); } + emit_num(mem); + emit_str(" kB"); + + // Name + emit_str(" "); + emit(&proc.name[..proc.name_len]); + + emit_str("\x1b[0m\n"); + } + + // Footer with kernel counters + emit_str("\n"); + emit_str(" Syscalls: "); + emit_formatted_num(syscalls); + emit_str(" | IRQs: "); + emit_formatted_num(interrupts); + emit_str(" | Ctx Sw: "); + emit_formatted_num(ctx_switches); + emit_str("\n"); + emit_str(" Forks: "); + emit_formatted_num(forks); + emit_str(" | Execs: "); + emit_formatted_num(execs); + emit_str(" | CoW: "); + emit_formatted_num(cow_faults); + emit_str("\n"); + + // Sleep 1 second before next refresh + let _ = libbreenix::time::sleep_ms(1000); + } +} diff --git a/userspace/programs/src/bwm.rs b/userspace/programs/src/bwm.rs new file mode 100644 index 00000000..00e2228b --- /dev/null +++ b/userspace/programs/src/bwm.rs @@ -0,0 +1,1035 @@ +//! Breenix Window Manager (bwm) +//! +//! Userspace window manager that provides tabbed terminal sessions. +//! Takes over the display from the kernel terminal manager and renders +//! its own tab bar + terminal content to the framebuffer. +//! +//! Tabs: +//! F1 - Shell (bsh) +//! F2 - Logs (kernel log viewer via /proc/kmsg) +//! F3 - Btop (system monitor) + +use std::process; + +use libbreenix::graphics; +use libbreenix::io; +use libbreenix::process::{fork, exec, waitpid, setsid, ForkResult, WNOHANG}; +use libbreenix::pty; +use libbreenix::types::Fd; + +use libgfx::color::Color; +use libgfx::font; +use libgfx::framebuf::FrameBuf; + +// ─── Constants ─────────────────────────────────────────────────────────────── + +/// Number of tabs +const NUM_TABS: usize = 3; + +/// Tab indices +const TAB_SHELL: usize = 0; +const TAB_LOGS: usize = 1; +const TAB_BTOP: usize = 2; + +/// Tab bar height in pixels +const TAB_BAR_HEIGHT: usize = 22; + +/// Font scale (libgfx 5x7 glyphs rendered at 2x = 10x14) +const FONT_SCALE: usize = 2; +const GLYPH_W: usize = 5 * FONT_SCALE; // 10px +const GLYPH_H: usize = 7 * FONT_SCALE; // 14px + +/// Cell padding +const CELL_PAD_X: usize = 0; +const CELL_PAD_Y: usize = 2; + +/// Cell dimensions +const CELL_W: usize = GLYPH_W + CELL_PAD_X; +const CELL_H: usize = GLYPH_H + CELL_PAD_Y; + +/// Colors +const BG_COLOR: Color = Color::rgb(10, 12, 30); +const FG_COLOR: Color = Color::rgb(200, 200, 200); +const TAB_BG: Color = Color::rgb(30, 40, 80); +const TAB_ACTIVE_BG: Color = Color::rgb(60, 80, 160); +const TAB_TEXT: Color = Color::rgb(200, 210, 240); +const CURSOR_COLOR: Color = Color::rgb(200, 200, 200); +const UNREAD_DOT: Color = Color::rgb(255, 60, 60); + +// ANSI color palette (standard 8 colors) +const ANSI_COLORS: [Color; 8] = [ + Color::rgb(0, 0, 0), // 0: black + Color::rgb(205, 49, 49), // 1: red + Color::rgb(13, 188, 121), // 2: green + Color::rgb(229, 229, 16), // 3: yellow + Color::rgb(36, 114, 200), // 4: blue + Color::rgb(188, 63, 188), // 5: magenta + Color::rgb(17, 168, 205), // 6: cyan + Color::rgb(229, 229, 229), // 7: white +]; + +// ─── Character Cell ────────────────────────────────────────────────────────── + +#[derive(Clone, Copy)] +struct Cell { + ch: u8, + fg: Color, + bg: Color, + bold: bool, +} + +impl Cell { + const fn blank() -> Self { + Self { + ch: b' ', + fg: FG_COLOR, + bg: BG_COLOR, + bold: false, + } + } +} + +// ─── ANSI Parser State Machine ─────────────────────────────────────────────── + +#[derive(Clone, Copy, PartialEq)] +enum AnsiState { + Normal, + Escape, // Got ESC + Csi, // Got ESC [ + CsiParam, // Collecting CSI parameters + OscString, // Got ESC ] (operating system command, skip until ST) +} + +// ─── Terminal Emulator (per-tab) ───────────────────────────────────────────── + +struct TermEmu { + cols: usize, + rows: usize, + cells: Vec, + cursor_x: usize, + cursor_y: usize, + fg: Color, + bg: Color, + bold: bool, + ansi_state: AnsiState, + ansi_params: [u16; 8], + ansi_param_idx: usize, + dirty: bool, +} + +impl TermEmu { + fn new(cols: usize, rows: usize) -> Self { + Self { + cols, + rows, + cells: vec![Cell::blank(); cols * rows], + cursor_x: 0, + cursor_y: 0, + fg: FG_COLOR, + bg: BG_COLOR, + bold: false, + ansi_state: AnsiState::Normal, + ansi_params: [0; 8], + ansi_param_idx: 0, + dirty: true, + } + } + + fn cell(&self, x: usize, y: usize) -> &Cell { + &self.cells[y * self.cols + x] + } + + fn cell_mut(&mut self, x: usize, y: usize) -> &mut Cell { + &mut self.cells[y * self.cols + x] + } + + /// Scroll the screen up by one line + fn scroll_up(&mut self) { + let cols = self.cols; + // Move lines up + for y in 1..self.rows { + for x in 0..cols { + self.cells[(y - 1) * cols + x] = self.cells[y * cols + x]; + } + } + // Clear bottom line + let last_row = self.rows - 1; + for x in 0..cols { + self.cells[last_row * cols + x] = Cell::blank(); + } + self.dirty = true; + } + + /// Put a character at cursor, advance cursor + fn put_char(&mut self, ch: u8) { + if self.cursor_x >= self.cols { + self.cursor_x = 0; + self.cursor_y += 1; + } + if self.cursor_y >= self.rows { + self.scroll_up(); + self.cursor_y = self.rows - 1; + } + let fg = self.fg; + let bg = self.bg; + let bold = self.bold; + let idx = self.cursor_y * self.cols + self.cursor_x; + self.cells[idx] = Cell { ch, fg, bg, bold }; + self.cursor_x += 1; + self.dirty = true; + } + + /// Process a single byte through the ANSI state machine + fn feed(&mut self, byte: u8) { + match self.ansi_state { + AnsiState::Normal => match byte { + 0x1b => self.ansi_state = AnsiState::Escape, + b'\n' => { + self.cursor_y += 1; + if self.cursor_y >= self.rows { + self.scroll_up(); + self.cursor_y = self.rows - 1; + } + self.dirty = true; + } + b'\r' => { + self.cursor_x = 0; + self.dirty = true; + } + b'\t' => { + let next_tab = (self.cursor_x + 8) & !7; + while self.cursor_x < next_tab && self.cursor_x < self.cols { + self.put_char(b' '); + } + } + 0x08 => { + // Backspace + if self.cursor_x > 0 { + self.cursor_x -= 1; + self.dirty = true; + } + } + 0x07 => {} // Bell - ignore + ch if ch >= 0x20 => self.put_char(ch), + _ => {} // Ignore other control characters + }, + AnsiState::Escape => match byte { + b'[' => { + self.ansi_state = AnsiState::Csi; + self.ansi_params = [0; 8]; + self.ansi_param_idx = 0; + } + b']' => { + self.ansi_state = AnsiState::OscString; + } + b'O' => { + // SS3 - skip next byte (function key) + self.ansi_state = AnsiState::Normal; + } + _ => { + self.ansi_state = AnsiState::Normal; + } + }, + AnsiState::Csi | AnsiState::CsiParam => { + if byte >= b'0' && byte <= b'9' { + self.ansi_state = AnsiState::CsiParam; + let idx = self.ansi_param_idx; + if idx < 8 { + self.ansi_params[idx] = self.ansi_params[idx] + .saturating_mul(10) + .saturating_add((byte - b'0') as u16); + } + } else if byte == b';' { + if self.ansi_param_idx < 7 { + self.ansi_param_idx += 1; + } + } else { + // Final byte - execute CSI command + let nparams = self.ansi_param_idx + 1; + self.execute_csi(byte, nparams); + self.ansi_state = AnsiState::Normal; + } + } + AnsiState::OscString => { + // Skip until BEL or ST (ESC \) + if byte == 0x07 || byte == b'\\' { + self.ansi_state = AnsiState::Normal; + } + } + } + } + + fn execute_csi(&mut self, cmd: u8, nparams: usize) { + let p0 = self.ansi_params[0] as usize; + let p1 = if nparams > 1 { self.ansi_params[1] as usize } else { 0 }; + + match cmd { + b'H' | b'f' => { + // CUP: Cursor Position (row;col, 1-based) + let row = if p0 > 0 { p0 - 1 } else { 0 }; + let col = if p1 > 0 { p1 - 1 } else { 0 }; + self.cursor_y = row.min(self.rows - 1); + self.cursor_x = col.min(self.cols - 1); + self.dirty = true; + } + b'A' => { + // CUU: Cursor Up + let n = if p0 > 0 { p0 } else { 1 }; + self.cursor_y = self.cursor_y.saturating_sub(n); + self.dirty = true; + } + b'B' => { + // CUD: Cursor Down + let n = if p0 > 0 { p0 } else { 1 }; + self.cursor_y = (self.cursor_y + n).min(self.rows - 1); + self.dirty = true; + } + b'C' => { + // CUF: Cursor Forward + let n = if p0 > 0 { p0 } else { 1 }; + self.cursor_x = (self.cursor_x + n).min(self.cols - 1); + self.dirty = true; + } + b'D' => { + // CUB: Cursor Back + let n = if p0 > 0 { p0 } else { 1 }; + self.cursor_x = self.cursor_x.saturating_sub(n); + self.dirty = true; + } + b'J' => { + // ED: Erase in Display + match p0 { + 0 => { + // Clear from cursor to end of screen + for x in self.cursor_x..self.cols { + *self.cell_mut(x, self.cursor_y) = Cell::blank(); + } + for y in (self.cursor_y + 1)..self.rows { + for x in 0..self.cols { + *self.cell_mut(x, y) = Cell::blank(); + } + } + } + 1 => { + // Clear from start to cursor + for y in 0..self.cursor_y { + for x in 0..self.cols { + *self.cell_mut(x, y) = Cell::blank(); + } + } + for x in 0..=self.cursor_x.min(self.cols - 1) { + *self.cell_mut(x, self.cursor_y) = Cell::blank(); + } + } + 2 | 3 => { + // Clear entire screen + for i in 0..self.cells.len() { + self.cells[i] = Cell::blank(); + } + } + _ => {} + } + self.dirty = true; + } + b'K' => { + // EL: Erase in Line + match p0 { + 0 => { + // Clear from cursor to end of line + for x in self.cursor_x..self.cols { + *self.cell_mut(x, self.cursor_y) = Cell::blank(); + } + } + 1 => { + // Clear from start to cursor + for x in 0..=self.cursor_x.min(self.cols - 1) { + *self.cell_mut(x, self.cursor_y) = Cell::blank(); + } + } + 2 => { + // Clear entire line + for x in 0..self.cols { + *self.cell_mut(x, self.cursor_y) = Cell::blank(); + } + } + _ => {} + } + self.dirty = true; + } + b'm' => { + // SGR: Select Graphic Rendition + for i in 0..nparams { + let code = self.ansi_params[i]; + match code { + 0 => { + self.fg = FG_COLOR; + self.bg = BG_COLOR; + self.bold = false; + } + 1 => self.bold = true, + 22 => self.bold = false, + 30..=37 => self.fg = ANSI_COLORS[(code - 30) as usize], + 39 => self.fg = FG_COLOR, + 40..=47 => self.bg = ANSI_COLORS[(code - 40) as usize], + 49 => self.bg = BG_COLOR, + 90..=97 => { + // Bright foreground colors + let mut c = ANSI_COLORS[(code - 90) as usize]; + c.r = c.r.saturating_add(55); + c.g = c.g.saturating_add(55); + c.b = c.b.saturating_add(55); + self.fg = c; + } + _ => {} // Ignore unsupported SGR codes + } + } + // Handle bare ESC[m (no params = reset) + if nparams == 1 && self.ansi_params[0] == 0 { + self.fg = FG_COLOR; + self.bg = BG_COLOR; + self.bold = false; + } + } + b'L' => { + // IL: Insert Lines + let n = if p0 > 0 { p0 } else { 1 }; + let cols = self.cols; + for _ in 0..n { + if self.cursor_y < self.rows - 1 { + // Shift lines down + for y in (self.cursor_y + 1..self.rows).rev() { + for x in 0..cols { + self.cells[y * cols + x] = self.cells[(y - 1) * cols + x]; + } + } + } + // Clear current line + for x in 0..cols { + self.cells[self.cursor_y * cols + x] = Cell::blank(); + } + } + self.dirty = true; + } + b'M' => { + // DL: Delete Lines + let n = if p0 > 0 { p0 } else { 1 }; + let cols = self.cols; + for _ in 0..n { + // Shift lines up + for y in self.cursor_y..self.rows - 1 { + for x in 0..cols { + self.cells[y * cols + x] = self.cells[(y + 1) * cols + x]; + } + } + // Clear bottom line + let last = self.rows - 1; + for x in 0..cols { + self.cells[last * cols + x] = Cell::blank(); + } + } + self.dirty = true; + } + b'G' => { + // CHA: Cursor Horizontal Absolute + let col = if p0 > 0 { p0 - 1 } else { 0 }; + self.cursor_x = col.min(self.cols - 1); + self.dirty = true; + } + b'd' => { + // VPA: Vertical Position Absolute + let row = if p0 > 0 { p0 - 1 } else { 0 }; + self.cursor_y = row.min(self.rows - 1); + self.dirty = true; + } + b'r' => { + // DECSTBM: Set Scrolling Region (ignored for now) + } + b'h' | b'l' => { + // SM/RM: Set/Reset Mode (ignored - includes cursor visibility etc.) + } + b'~' => { + // Special keys (F1~, F2~, etc.) - handled at input level + } + _ => {} // Ignore unknown CSI commands + } + } + + /// Feed a slice of bytes through the emulator + fn feed_bytes(&mut self, data: &[u8]) { + for &b in data { + self.feed(b); + } + } + + /// Render the terminal emulator content to a framebuffer region + fn render(&mut self, fb: &mut FrameBuf, x_off: usize, y_off: usize) { + if !self.dirty { + return; + } + self.dirty = false; + + for row in 0..self.rows { + for col in 0..self.cols { + let cell = self.cell(col, row); + let px = x_off + col * CELL_W; + let py = y_off + row * CELL_H; + + // Draw background + for dy in 0..CELL_H { + for dx in 0..CELL_W { + fb.put_pixel(px + dx, py + dy, cell.bg); + } + } + + // Draw character + if cell.ch > b' ' && cell.ch < 127 { + let fg = if cell.bold { + Color::rgb( + cell.fg.r.saturating_add(55), + cell.fg.g.saturating_add(55), + cell.fg.b.saturating_add(55), + ) + } else { + cell.fg + }; + font::draw_char(fb, cell.ch, px, py + 1, fg, FONT_SCALE); + } + } + } + + // Draw cursor + if self.cursor_x < self.cols && self.cursor_y < self.rows { + let cx = x_off + self.cursor_x * CELL_W; + let cy = y_off + self.cursor_y * CELL_H + CELL_H - 2; + for dx in 0..CELL_W { + fb.put_pixel(cx + dx, cy, CURSOR_COLOR); + fb.put_pixel(cx + dx, cy + 1, CURSOR_COLOR); + } + } + } +} + +// ─── Tab State ─────────────────────────────────────────────────────────────── + +struct Tab { + name: &'static str, + shortcut: &'static str, + emu: TermEmu, + master_fd: Option, + child_pid: i64, + has_unread: bool, +} + +// ─── Escape Sequence Parser for Keyboard Input ────────────────────────────── + +enum KeyEvent { + Char(u8), + F1, + F2, + F3, + None, +} + +struct InputParser { + buf: [u8; 8], + len: usize, +} + +impl InputParser { + fn new() -> Self { + Self { buf: [0; 8], len: 0 } + } + + fn feed(&mut self, byte: u8) -> KeyEvent { + self.buf[self.len] = byte; + self.len += 1; + + // Check for escape sequences + if self.buf[0] == 0x1b { + if self.len == 1 { + return KeyEvent::None; // Wait for more bytes + } + if self.len == 2 { + if self.buf[1] == b'[' || self.buf[1] == b'O' { + return KeyEvent::None; // Wait for more + } + // ESC + something else: return ESC then the byte + self.len = 0; + return KeyEvent::Char(byte); + } + if self.len >= 3 { + let result = match &self.buf[..self.len] { + [0x1b, b'O', b'P'] => KeyEvent::F1, + [0x1b, b'O', b'Q'] => KeyEvent::F2, + [0x1b, b'O', b'R'] => KeyEvent::F3, + [0x1b, b'[', b'1', b'1', b'~'] if self.len == 5 => KeyEvent::F1, + [0x1b, b'[', b'1', b'2', b'~'] if self.len == 5 => KeyEvent::F2, + [0x1b, b'[', b'1', b'3', b'~'] if self.len == 5 => KeyEvent::F3, + _ => { + // Incomplete or unrecognized, wait for more or timeout + if self.len >= 6 { + // Too long, give up + self.len = 0; + return KeyEvent::Char(byte); + } + return KeyEvent::None; + } + }; + self.len = 0; + return result; + } + } + + // Regular character + self.len = 0; + KeyEvent::Char(byte) + } + + /// Flush pending escape sequence as individual chars + fn flush(&mut self) -> Option { + if self.len > 0 { + let byte = self.buf[0]; + // Shift buffer + for i in 0..self.len - 1 { + self.buf[i] = self.buf[i + 1]; + } + self.len -= 1; + Some(byte) + } else { + None + } + } +} + +// ─── Helper Functions ──────────────────────────────────────────────────────── + +/// Spawn a child process connected to a PTY +fn spawn_child(path: &[u8], _name: &str) -> (Fd, i64) { + // Create PTY pair + let (master_fd, slave_path) = match pty::openpty() { + Ok(pair) => pair, + Err(_) => return (Fd::from_raw(0), -1), + }; + + let slave_path_slice = pty::slave_path_bytes(&slave_path); + + match fork() { + Ok(ForkResult::Child) => { + // Child process: set up PTY slave as stdin/stdout/stderr + let _ = setsid(); // New session + + // Close the master fd in child + let _ = io::close(master_fd); + + // Build null-terminated path for open + let mut open_path = [0u8; 64]; + let copy_len = slave_path_slice.len().min(63); + open_path[..copy_len].copy_from_slice(&slave_path_slice[..copy_len]); + + let path_str = core::str::from_utf8(&open_path[..copy_len]).unwrap_or("/dev/pts/0"); + + // Open the slave PTY + let slave_fd = match libbreenix::fs::open(path_str, 0x02) { + // O_RDWR + Ok(fd) => fd, + Err(_) => { + libbreenix::process::exit(126); + } + }; + + // Dup to stdin/stdout/stderr + let _ = io::dup2(slave_fd, Fd::from_raw(0)); + let _ = io::dup2(slave_fd, Fd::from_raw(1)); + let _ = io::dup2(slave_fd, Fd::from_raw(2)); + + // Close original if it's not 0, 1, or 2 + if slave_fd.raw() > 2 { + let _ = io::close(slave_fd); + } + + // Exec the program + let _ = exec(path); + libbreenix::process::exit(127); + } + Ok(ForkResult::Parent(child_pid)) => { + (master_fd, child_pid.raw() as i64) + } + Err(_) => { + let _ = io::close(master_fd); + (Fd::from_raw(0), -1) + } + } +} + +/// Read /proc/kmsg for kernel log content +fn read_kmsg() -> Vec { + let mut buf = vec![0u8; 4096]; + match libbreenix::fs::open("/proc/kmsg", 0) { + Ok(fd) => { + let mut total = 0; + loop { + match io::read(fd, &mut buf[total..]) { + Ok(n) if n > 0 => { + total += n; + if total >= buf.len() - 256 { + buf.resize(buf.len() + 4096, 0); + } + } + _ => break, + } + } + let _ = io::close(fd); + buf.truncate(total); + buf + } + Err(_) => Vec::new(), + } +} + +// ─── Tab Bar Rendering ─────────────────────────────────────────────────────── + +fn draw_tab_bar(fb: &mut FrameBuf, tabs: &[Tab], active: usize, width: usize) { + // Background + for y in 0..TAB_BAR_HEIGHT { + for x in 0..width { + fb.put_pixel(x, y, TAB_BG); + } + } + + let tab_w = width / NUM_TABS; + for (i, tab) in tabs.iter().enumerate() { + let tx = i * tab_w; + let bg = if i == active { TAB_ACTIVE_BG } else { TAB_BG }; + + // Tab background + for y in 1..TAB_BAR_HEIGHT - 1 { + for x in tx + 1..tx + tab_w - 1 { + if x < width { + fb.put_pixel(x, y, bg); + } + } + } + + // Tab label: "Shell [F1]" + let mut label = [0u8; 20]; + let mut lp = 0; + for &b in tab.name.as_bytes() { + if lp < 20 { label[lp] = b; lp += 1; } + } + if lp < 18 { + label[lp] = b' '; lp += 1; + label[lp] = b'['; lp += 1; + for &b in tab.shortcut.as_bytes() { + if lp < 19 { label[lp] = b; lp += 1; } + } + if lp < 20 { label[lp] = b']'; lp += 1; } + } + + let text_x = tx + 6; + let text_y = 4; + font::draw_text(fb, &label[..lp], text_x, text_y, TAB_TEXT, FONT_SCALE); + + // Unread indicator + if tab.has_unread && i != active { + let dot_x = tx + tab_w - 12; + let dot_y = TAB_BAR_HEIGHT / 2; + for dy in 0..4_usize { + for dx in 0..4_usize { + fb.put_pixel(dot_x + dx, dot_y - 2 + dy, UNREAD_DOT); + } + } + } + } + + // Bottom border + for x in 0..width { + fb.put_pixel(x, TAB_BAR_HEIGHT - 1, Color::rgb(80, 100, 180)); + } +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +fn main() { + print!("[bwm] Breenix Window Manager starting...\n"); + + // Step 1: Take over the display from kernel terminal manager + if let Err(e) = graphics::take_over_display() { + print!("[bwm] WARNING: take_over_display failed: {}\n", e); + } + + // Step 2: Get framebuffer info and mmap + let info = match graphics::fbinfo() { + Ok(info) => info, + Err(e) => { + print!("[bwm] ERROR: fbinfo failed: {}\n", e); + process::exit(1); + } + }; + + let fb_ptr = match graphics::fb_mmap() { + Ok(ptr) => ptr, + Err(e) => { + print!("[bwm] ERROR: fb_mmap failed: {}\n", e); + process::exit(1); + } + }; + + // After take_over_display, fb_mmap maps the full screen (not just left pane) + let width = info.width as usize; + let height = info.height as usize; + let bpp = info.bytes_per_pixel as usize; + + let mut fb = unsafe { + FrameBuf::from_raw( + fb_ptr, + width, + height, + width * bpp, + bpp, + info.is_bgr(), + ) + }; + + // Calculate terminal dimensions + let term_y_offset = TAB_BAR_HEIGHT; + let term_height = height - term_y_offset; + let term_cols = width / CELL_W; + let term_rows = term_height / CELL_H; + + print!("[bwm] Display: {}x{}, terminal: {}x{} cells\n", width, height, term_cols, term_rows); + + // Step 3: Enter raw mode on stdin + let mut orig_termios = libbreenix::termios::Termios::default(); + let _ = libbreenix::termios::tcgetattr(Fd::from_raw(0), &mut orig_termios); + let mut raw = orig_termios; + libbreenix::termios::cfmakeraw(&mut raw); + let _ = libbreenix::termios::tcsetattr(Fd::from_raw(0), libbreenix::termios::TCSANOW, &raw); + + // Step 4: Create tabs with PTY children + let (shell_master, shell_pid) = spawn_child(b"/bin/bsh\0", "bsh"); + let (btop_master, btop_pid) = spawn_child(b"/bin/btop\0", "btop"); + + print!("[bwm] Shell PID: {}, btop PID: {}\n", shell_pid, btop_pid); + + let mut tabs = [ + Tab { + name: "Shell", + shortcut: "F1", + emu: TermEmu::new(term_cols, term_rows), + master_fd: Some(shell_master), + child_pid: shell_pid, + has_unread: false, + }, + Tab { + name: "Logs", + shortcut: "F2", + emu: TermEmu::new(term_cols, term_rows), + master_fd: None, // Logs tab reads /proc/kmsg directly + child_pid: -1, + has_unread: false, + }, + Tab { + name: "Btop", + shortcut: "F3", + emu: TermEmu::new(term_cols, term_rows), + master_fd: Some(btop_master), + child_pid: btop_pid, + has_unread: false, + }, + ]; + + let mut active_tab: usize = TAB_SHELL; + let mut input_parser = InputParser::new(); + let mut kmsg_offset: usize = 0; // Track how much of /proc/kmsg we've shown + let mut frame: u32 = 0; + + // Initial render + fb.clear(BG_COLOR); + draw_tab_bar(&mut fb, &tabs, active_tab, width); + tabs[active_tab].emu.render(&mut fb, 0, term_y_offset); + let _ = graphics::fb_flush(); + + // Step 5: Main loop + let mut read_buf = [0u8; 512]; + let mut needs_flush = false; + + // Pre-allocate poll fds (avoid Vec allocation per frame) + let mut poll_fds = [ + io::PollFd { fd: 0, events: io::poll_events::POLLIN as i16, revents: 0 }, + io::PollFd { fd: 0, events: io::poll_events::POLLIN as i16, revents: 0 }, + io::PollFd { fd: 0, events: io::poll_events::POLLIN as i16, revents: 0 }, + ]; + + loop { + // Reset revents and set up poll fds + let mut nfds = 1; // Always poll stdin (index 0) + poll_fds[0].revents = 0; + + // Shell PTY master + let shell_poll_idx = if let Some(fd) = tabs[TAB_SHELL].master_fd { + poll_fds[nfds].fd = fd.raw() as i32; + poll_fds[nfds].revents = 0; + let idx = nfds; + nfds += 1; + Some(idx) + } else { + None + }; + + // Btop PTY master + let btop_poll_idx = if let Some(fd) = tabs[TAB_BTOP].master_fd { + poll_fds[nfds].fd = fd.raw() as i32; + poll_fds[nfds].revents = 0; + let idx = nfds; + nfds += 1; + Some(idx) + } else { + None + }; + + // Poll with 100ms timeout (for periodic /proc/kmsg reads) + let _nready = io::poll(&mut poll_fds[..nfds], 100).unwrap_or(0); + + // Check stdin + if poll_fds[0].revents & (io::poll_events::POLLIN as i16) != 0 { + match io::read(Fd::from_raw(0), &mut read_buf) { + Ok(n) if n > 0 => { + for i in 0..n { + match input_parser.feed(read_buf[i]) { + KeyEvent::F1 => { + if active_tab != TAB_SHELL { + active_tab = TAB_SHELL; + tabs[TAB_SHELL].has_unread = false; + tabs[active_tab].emu.dirty = true; + draw_tab_bar(&mut fb, &tabs, active_tab, width); + needs_flush = true; + } + } + KeyEvent::F2 => { + if active_tab != TAB_LOGS { + active_tab = TAB_LOGS; + tabs[TAB_LOGS].has_unread = false; + tabs[active_tab].emu.dirty = true; + draw_tab_bar(&mut fb, &tabs, active_tab, width); + needs_flush = true; + } + } + KeyEvent::F3 => { + if active_tab != TAB_BTOP { + active_tab = TAB_BTOP; + tabs[TAB_BTOP].has_unread = false; + tabs[active_tab].emu.dirty = true; + draw_tab_bar(&mut fb, &tabs, active_tab, width); + needs_flush = true; + } + } + KeyEvent::Char(ch) => { + // Forward to active tab's PTY master + if let Some(fd) = tabs[active_tab].master_fd { + let _ = io::write(fd, &[ch]); + } + } + KeyEvent::None => {} // Waiting for more input + } + } + } + _ => {} + } + } + + // Flush any pending escape sequence bytes after a timeout + while let Some(_byte) = input_parser.flush() { + // Could forward to PTY, but usually these are partial escapes + } + + // Check Shell PTY master + if let Some(idx) = shell_poll_idx { + if poll_fds[idx].revents & (io::poll_events::POLLIN as i16) != 0 { + if let Some(fd) = tabs[TAB_SHELL].master_fd { + match io::read(fd, &mut read_buf) { + Ok(n) if n > 0 => { + tabs[TAB_SHELL].emu.feed_bytes(&read_buf[..n]); + if active_tab != TAB_SHELL { + tabs[TAB_SHELL].has_unread = true; + } + } + _ => {} + } + } + } + } + + // Check Btop PTY master + if let Some(idx) = btop_poll_idx { + if poll_fds[idx].revents & (io::poll_events::POLLIN as i16) != 0 { + if let Some(fd) = tabs[TAB_BTOP].master_fd { + match io::read(fd, &mut read_buf) { + Ok(n) if n > 0 => { + tabs[TAB_BTOP].emu.feed_bytes(&read_buf[..n]); + if active_tab != TAB_BTOP { + tabs[TAB_BTOP].has_unread = true; + } + } + _ => {} + } + } + } + } + + // Periodic: read /proc/kmsg for Logs tab (~every 10 frames = ~1 Hz) + if frame % 10 == 0 { + let kmsg = read_kmsg(); + if kmsg.len() > kmsg_offset { + let new_data = &kmsg[kmsg_offset..]; + tabs[TAB_LOGS].emu.feed_bytes(new_data); + kmsg_offset = kmsg.len(); + if active_tab != TAB_LOGS { + tabs[TAB_LOGS].has_unread = true; + } + } + } + + // Reap dead children (non-blocking) + let mut status: i32 = 0; + loop { + match waitpid(-1, &mut status as *mut i32, WNOHANG) { + Ok(pid) if pid.raw() > 0 => { + let rpid = pid.raw() as i64; + // Check if a child died and respawn + if rpid == tabs[TAB_SHELL].child_pid { + print!("[bwm] Shell exited, respawning...\n"); + let (m, p) = spawn_child(b"/bin/bsh\0", "bsh"); + tabs[TAB_SHELL].master_fd = Some(m); + tabs[TAB_SHELL].child_pid = p; + } else if rpid == tabs[TAB_BTOP].child_pid { + print!("[bwm] btop exited, respawning...\n"); + let (m, p) = spawn_child(b"/bin/btop\0", "btop"); + tabs[TAB_BTOP].master_fd = Some(m); + tabs[TAB_BTOP].child_pid = p; + } + } + _ => break, + } + } + + // Render active tab (only if dirty) + if tabs[active_tab].emu.dirty { + tabs[active_tab].emu.render(&mut fb, 0, term_y_offset); + needs_flush = true; + } + + // Redraw tab bar periodically (for unread indicators) + if frame % 10 == 0 { + draw_tab_bar(&mut fb, &tabs, active_tab, width); + needs_flush = true; + } + + // Only flush when something changed + if needs_flush { + let _ = graphics::fb_flush(); + needs_flush = false; + } + + frame = frame.wrapping_add(1); + } +} diff --git a/userspace/programs/src/init.rs b/userspace/programs/src/init.rs index f6dd02bf..20467e15 100644 --- a/userspace/programs/src/init.rs +++ b/userspace/programs/src/init.rs @@ -1,10 +1,10 @@ //! Breenix init process (/sbin/init) - std version //! -//! PID 1 - spawns system services and the interactive shell, then reaps zombies. +//! PID 1 - spawns system services and the shell, then reaps zombies. //! //! Spawns: //! - /sbin/telnetd (background service, optional) -//! - /bin/bsh (Breenish ECMAScript shell, foreground on serial console) +//! - /bin/bsh (Breenix Shell) //! //! Main loop reaps terminated children with waitpid(WNOHANG) and respawns //! crashed services with backoff to prevent tight respawn loops. @@ -61,7 +61,7 @@ fn main() { let mut telnetd_pid = spawn(TELNETD_PATH, "telnetd"); let mut telnetd_failures: u32 = 0; - // Start Breenish shell + // Start the shell directly print!("[init] Starting /bin/bsh...\n"); let mut shell_pid = spawn(SHELL_PATH, "bsh"); let mut shell_failures: u32 = 0;