launcher/main.rs
1//! # R.E.K.T.A.Launcher Application
2//!
3//! Entry point and GUI application for starting, monitoring, and managing
4//! REKTAL Kernel server instances and GUI client processes.
5//!
6//! ## Overview
7//! The launcher provides an [`eframe`]/[`egui`]-based user interface allowing developers
8//! and users to:
9//! - Spawn the **Kernel** server with or without an interactive terminal console.
10//! - Spawn the **GUI** client with or without an interactive terminal console.
11//! - Launch processes in a fully detached mode so they outlive the launcher.
12//! - Observe status feedback and application logs via [`common::logging`].
13
14mod spawn_process;
15
16use crate::spawn_process::{spawn_gui, spawn_kernel};
17use common::logging::LogLevel::*;
18use common::logging::{FileSink, Logger, TerminalSink};
19use common::r_log;
20use eframe::egui;
21use sysinfo::{ProcessesToUpdate, System};
22
23/// Application entry point for R.E.K.T.A.Launcher.
24///
25/// Sets up the global logger sinks (`/tmp/rektal_launcher.log` and terminal stdout),
26/// configures the native window viewport options, and initializes the [`eframe`] event loop.
27///
28/// # Errors
29///
30/// Returns an [`eframe::Result`] if native window creation or event loop initialization fails.
31fn main() -> eframe::Result<()> {
32 Logger::global().add_sink(Box::new(FileSink::new("rektal_launcher.log")));
33 Logger::global().add_sink(Box::new(TerminalSink { cli_prompt: None }));
34
35 let options = eframe::NativeOptions {
36 viewport: egui::ViewportBuilder::default()
37 .with_inner_size([400.0, 320.0])
38 .with_title("R.E.K.T.A.Launcher")
39 .with_resizable(false),
40 ..Default::default()
41 };
42
43 r_log!(Info, "eframe initialized");
44
45 eframe::run_native(
46 "R.E.K.T.A.Launcher",
47 options,
48 Box::new(|cc| {
49 Ok(Box::<LauncherApp>::new(LauncherApp::new(
50 cc.egui_ctx.clone(),
51 )))
52 }),
53 )
54}
55
56/// Primary state structure for the Launcher GUI application.
57struct LauncherApp {
58 /// Human-readable status message displayed in the UI for user feedback.
59 status_msg: String,
60 /// Whether to open a visible console window when spawning the Kernel.
61 show_kernel_console: bool,
62 /// Whether to open a visible console window when spawning the GUI client.
63 show_gui_console: bool,
64 /// System monitor instance used for querying running processes.
65 system: System,
66}
67
68impl LauncherApp {
69 /// Creates a new [`LauncherApp`] instance with default state, initializes the [`System`] monitor,
70 /// and resolves the workspace directory.
71 ///
72 /// # Arguments
73 ///
74 /// * `_ctx` - The egui context used for UI rendering and state configuration.
75 fn new(_ctx: egui::Context) -> Self {
76 let current_dir = std::env::current_dir().unwrap_or_default();
77 let mut search_dir = current_dir.clone();
78 let mut workspace_dir = current_dir.clone();
79
80 // Search parent directories for the workspace folder containing 'gui' or 'kernel' Cargo.toml
81 for _ in 0..5 {
82 if search_dir.join("rektal_client").join("Cargo.toml").exists()
83 || search_dir.join("rektal_kernel").join("Cargo.toml").exists()
84 {
85 workspace_dir = search_dir.join("rektal_client");
86 break;
87 }
88 if search_dir.join("Cargo.toml").exists() && search_dir.join("rektal_client").exists() {
89 workspace_dir = search_dir.join("rektal_client");
90 break;
91 }
92 if let Some(parent) = search_dir.parent() {
93 search_dir = parent.to_path_buf();
94 } else {
95 break;
96 }
97 }
98
99 r_log!(Info, "Resolved workspace directory: {:?}", workspace_dir);
100
101 let sys = System::new();
102
103 Self {
104 status_msg: String::new(),
105 show_kernel_console: false,
106 show_gui_console: false,
107 system: sys,
108 }
109 }
110
111 /// Renders the UI section for configuring and launching the REKTAL Kernel.
112 ///
113 /// Includes a live status indicator (green/red dot), a checkbox for toggling the
114 /// terminal console, and a button to spawn the process if not already running.
115 ///
116 /// # Arguments
117 ///
118 /// * `ui` - The egui UI builder used to draw the controls.
119 fn draw_kernel_group(&mut self, ui: &mut egui::Ui) {
120 ui.group(|ui| {
121 ui.vertical(|ui| {
122 ui.horizontal(|ui| {
123 let color = if self.is_kernel_active() {
124 egui::Color32::from_rgb(46, 204, 113) // Green
125 } else {
126 egui::Color32::from_rgb(231, 76, 60) // Red
127 };
128 let (rect, _) =
129 ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
130 ui.painter().circle_filled(rect.center(), 5.0, color);
131 ui.label(egui::RichText::new("Kernel:").strong());
132 });
133
134 ui.add_space(5.0);
135
136 ui.checkbox(&mut self.show_kernel_console, "show kernel console");
137
138 ui.add_space(5.0);
139
140
141
142 if ui.add_enabled(!self.is_kernel_active(), egui::Button::new("▶ Start Kernel")).clicked() {
143 if !self.is_kernel_active() {
144 match spawn_kernel(&[], self.show_kernel_console) {
145 Ok(_) => {
146 self.status_msg = "Kernel successfully spawned!".to_string();
147 r_log!(SuccessEvent, "Kernel successfully spawned!");
148 }
149 Err(e) => {
150 self.status_msg = format!("Failed to spawn Kernel: {}", e);
151 r_log!(Error, "Kernel couldn't be spawned: {}", e);
152 }
153 }
154 } else {
155 self.status_msg = "Kernel already spawned!".to_string();
156 }
157 }
158 })
159 });
160 }
161
162 /// Renders the UI section for configuring and launching the REKTAL GUI client.
163 ///
164 /// Includes a live status indicator (green/red dot), a checkbox for toggling the
165 /// terminal console, and a button to spawn the process.
166 ///
167 /// # Arguments
168 ///
169 /// * `ui` - The egui UI builder used to draw the controls.
170 fn draw_gui_group(&mut self, ui: &mut egui::Ui) {
171 ui.group(|ui| {
172 ui.vertical(|ui| {
173 ui.horizontal(|ui| {
174 let color = if self.is_gui_active() {
175 egui::Color32::from_rgb(46, 204, 113) // Green
176 } else {
177 egui::Color32::from_rgb(231, 76, 60) // Red
178 };
179 let (rect, _) =
180 ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
181 ui.painter().circle_filled(rect.center(), 5.0, color);
182 ui.label(egui::RichText::new("GUI:").strong());
183 });
184
185 ui.add_space(5.0);
186
187 ui.checkbox(&mut self.show_gui_console, "show gui console");
188
189 ui.add_space(5.0);
190
191 if ui.add_enabled(!self.is_gui_active(), egui::Button::new("▶ Start GUI")).clicked() {
192 match spawn_gui(&[], self.show_gui_console) {
193 Ok(_) => {
194 self.status_msg = "GUI successfully spawned!".to_string();
195 r_log!(SuccessEvent, "GUI successfully spawned!");
196 }
197 Err(e) => {
198 self.status_msg = format!("Failed to spawn GUI: {}", e);
199 r_log!(Error, "GUI couldn't be spawned: {}", e);
200 }
201 }
202 }
203 })
204 });
205 }
206
207 /// Checks whether a REKTAL Kernel process is currently active on the host system.
208 ///
209 /// Iterates through the active system processes cached in [`System`] to check if any process
210 /// name begins with `"kernel"`.
211 ///
212 /// # Returns
213 ///
214 /// `true` if a matching Kernel process is active, `false` otherwise.
215 fn is_kernel_active(&self) -> bool {
216 self.system
217 .processes()
218 .values()
219 .any(|p| p.name().to_string_lossy().starts_with("rektal_kernel"))
220 }
221
222 /// Checks whether a REKTAL GUI client process is currently active on the host system.
223 ///
224 /// Iterates through the active system processes cached in [`System`] to check if any process
225 /// name begins with `"gui"`.
226 ///
227 /// # Returns
228 ///
229 /// `true` if a matching GUI client process is active, `false` otherwise.
230 fn is_gui_active(&self) -> bool {
231 self.system
232 .processes()
233 .values()
234 .any(|p| p.name().to_string_lossy().starts_with("rektal_client"))
235 }
236}
237
238impl eframe::App for LauncherApp {
239 /// Primary update function called once per UI frame by `eframe`.
240 ///
241 /// Renders the central panel with side-by-side controls and live status dots for Kernel and GUI,
242 /// presents active status messages, refreshes the OS process list, and schedules automatic repaints every 500ms.
243 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
244 egui::CentralPanel::default().show(ctx, |ui| {
245 ui.horizontal(|ui| {
246 self.draw_kernel_group(ui);
247
248 ui.add_space(10.0);
249
250 self.draw_gui_group(ui);
251 });
252 if !self.status_msg.is_empty() {
253 ui.colored_label(egui::Color32::LIGHT_BLUE, &self.status_msg);
254 }
255 });
256 self.system.refresh_processes(ProcessesToUpdate::All, true);
257
258 ctx.request_repaint_after(std::time::Duration::from_millis(500));
259 }
260}