Skip to main content

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}