⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
launcher.rs

1//! Application Launcher
2//!
3//! Provides an application launcher overlay with search functionality,
4//! grid display, and keyboard navigation. The launcher renders as a
5//! semi-transparent overlay on top of the desktop, showing available
6//! applications in a grid layout. Users can search by typing, navigate
7//! with arrow keys, and launch applications with Enter.
8
9#![allow(dead_code)]
10
11use alloc::{string::String, vec, vec::Vec};
12
13use crate::sync::once_lock::GlobalState;
14
15// ---------------------------------------------------------------------------
16// Types
17// ---------------------------------------------------------------------------
18
19/// Application category for grouping and icon color coding.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AppCategory {
22    /// Core system utilities (task manager, system monitor)
23    System,
24    /// General-purpose utilities (calculator, clock)
25    Utility,
26    /// Development tools (editors, compilers, debuggers)
27    Development,
28    /// Graphics and image applications
29    Graphics,
30    /// Networking and internet applications
31    Network,
32    /// Audio, video, and media applications
33    Multimedia,
34    /// Office and productivity applications
35    Office,
36    /// System configuration and preferences
37    Settings,
38    /// Uncategorized applications
39    Other,
40}
41
42/// A single application entry in the launcher.
43#[derive(Debug, Clone)]
44pub struct AppEntry {
45    /// Human-readable application name.
46    pub name: String,
47    /// Path to the executable binary.
48    pub exec_path: String,
49    /// Icon name (used for future icon theme lookup).
50    pub icon_name: String,
51    /// Application category.
52    pub category: AppCategory,
53    /// Short description of the application.
54    pub description: String,
55}
56
57impl AppEntry {
58    /// Create a new application entry.
59    pub fn new(
60        name: &str,
61        exec_path: &str,
62        icon_name: &str,
63        category: AppCategory,
64        description: &str,
65    ) -> Self {
66        Self {
67            name: String::from(name),
68            exec_path: String::from(exec_path),
69            icon_name: String::from(icon_name),
70            category,
71            description: String::from(description),
72        }
73    }
74}
75
76/// Launcher visibility state.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum LauncherState {
79    /// Launcher is not visible.
80    Hidden,
81    /// Launcher is visible, showing the application grid.
82    Visible,
83    /// Launcher is visible and the search bar is actively receiving input.
84    SearchActive,
85}
86
87/// Action returned by input handlers to the caller.
88#[derive(Debug, Clone)]
89pub enum LauncherAction {
90    /// Launch the application at the given exec path.
91    Launch(String),
92    /// Hide the launcher overlay.
93    Hide,
94}
95
96// ---------------------------------------------------------------------------
97// Launcher
98// ---------------------------------------------------------------------------
99
100/// Application launcher with search, grid display, and keyboard navigation.
101pub struct AppLauncher {
102    /// All registered application entries.
103    entries: Vec<AppEntry>,
104    /// Indices into `entries` for the current filter result.
105    filtered: Vec<usize>,
106    /// Current search query string.
107    search_query: String,
108    /// Index into `filtered` of the currently selected (highlighted) entry.
109    selected_index: usize,
110    /// Current launcher state.
111    state: LauncherState,
112    /// Number of columns in the application grid.
113    grid_columns: usize,
114    /// Number of visible rows in the application grid.
115    grid_rows: usize,
116    /// Scroll offset (in number of entries) for the grid.
117    scroll_offset: usize,
118    /// Overlay X position in pixels (relative to screen).
119    overlay_x: usize,
120    /// Overlay Y position in pixels (relative to screen).
121    overlay_y: usize,
122    /// Overlay width in pixels.
123    overlay_width: usize,
124    /// Overlay height in pixels.
125    overlay_height: usize,
126}
127
128impl AppLauncher {
129    /// Create a new application launcher pre-populated with default
130    /// applications.
131    pub fn new() -> Self {
132        let entries = default_applications();
133        let filtered: Vec<usize> = (0..entries.len()).collect();
134
135        Self {
136            entries,
137            filtered,
138            search_query: String::new(),
139            selected_index: 0,
140            state: LauncherState::Hidden,
141            grid_columns: 4,
142            grid_rows: 3,
143            scroll_offset: 0,
144            overlay_x: 0,
145            overlay_y: 0,
146            overlay_width: 640,
147            overlay_height: 480,
148        }
149    }
150
151    /// Show the launcher overlay, clearing any previous search.
152    pub fn show(&mut self) {
153        self.state = LauncherState::Visible;
154        self.search_query.clear();
155        self.selected_index = 0;
156        self.scroll_offset = 0;
157        // Reset filter to show all entries
158        self.filtered = (0..self.entries.len()).collect();
159    }
160
161    /// Hide the launcher overlay.
162    pub fn hide(&mut self) {
163        self.state = LauncherState::Hidden;
164    }
165
166    /// Toggle the launcher between visible and hidden.
167    pub fn toggle(&mut self) {
168        match self.state {
169            LauncherState::Hidden => self.show(),
170            LauncherState::Visible | LauncherState::SearchActive => self.hide(),
171        }
172    }
173
174    /// Returns `true` if the launcher is currently visible.
175    pub fn is_visible(&self) -> bool {
176        self.state != LauncherState::Hidden
177    }
178
179    /// Set the overlay position and dimensions (called when screen size is
180    /// known).
181    pub fn set_overlay_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
182        self.overlay_x = x;
183        self.overlay_y = y;
184        self.overlay_width = width;
185        self.overlay_height = height;
186    }
187
188    /// Register a new application entry with the launcher.
189    pub fn register_app(&mut self, entry: AppEntry) {
190        self.entries.push(entry);
191        // Refresh filter
192        self.filter_entries();
193    }
194
195    /// Remove an application by exec path.
196    pub fn unregister_app(&mut self, exec_path: &str) {
197        self.entries.retain(|e| e.exec_path.as_str() != exec_path);
198        self.filter_entries();
199    }
200
201    /// Handle a keyboard input event.
202    ///
203    /// Returns an optional `LauncherAction` indicating what the caller should
204    /// do (launch an app, hide the launcher, or nothing).
205    ///
206    /// Key mappings:
207    /// - Enter (0x0A or 0x0D): launch the selected application
208    /// - Escape (0x1B): hide the launcher
209    /// - Backspace (0x08): delete last character from search query
210    /// - Arrow Up (0x80): move selection up one row
211    /// - Arrow Down (0x81): move selection down one row
212    /// - Arrow Left (0x82): move selection left one column
213    /// - Arrow Right (0x83): move selection right one column
214    /// - Printable ASCII (0x20..=0x7E): append to search query, filter entries
215    pub fn handle_key(&mut self, key: u8) -> Option<LauncherAction> {
216        if self.state == LauncherState::Hidden {
217            return None;
218        }
219
220        match key {
221            // Enter -- launch selected
222            0x0A | 0x0D => {
223                if let Some(entry) = self.selected_entry() {
224                    let path = entry.exec_path.clone();
225                    self.hide();
226                    return Some(LauncherAction::Launch(path));
227                }
228                None
229            }
230            // Escape -- hide
231            0x1B => {
232                self.hide();
233                Some(LauncherAction::Hide)
234            }
235            // Backspace -- delete last search char
236            0x08 => {
237                if !self.search_query.is_empty() {
238                    self.search_query.pop();
239                    self.filter_entries();
240                    self.clamp_selection();
241                }
242                // If search is now empty, revert to Visible state
243                if self.search_query.is_empty() {
244                    self.state = LauncherState::Visible;
245                }
246                None
247            }
248            // Arrow Up
249            0x80 => {
250                if self.selected_index >= self.grid_columns {
251                    self.selected_index -= self.grid_columns;
252                    self.ensure_visible();
253                }
254                None
255            }
256            // Arrow Down
257            0x81 => {
258                let new_idx = self.selected_index + self.grid_columns;
259                if new_idx < self.filtered.len() {
260                    self.selected_index = new_idx;
261                    self.ensure_visible();
262                }
263                None
264            }
265            // Arrow Left
266            0x82 => {
267                if self.selected_index > 0 {
268                    self.selected_index -= 1;
269                    self.ensure_visible();
270                }
271                None
272            }
273            // Arrow Right
274            0x83 => {
275                if self.selected_index + 1 < self.filtered.len() {
276                    self.selected_index += 1;
277                    self.ensure_visible();
278                }
279                None
280            }
281            // Printable ASCII -- add to search
282            0x20..=0x7E => {
283                self.state = LauncherState::SearchActive;
284                self.search_query.push(key as char);
285                self.filter_entries();
286                self.clamp_selection();
287                None
288            }
289            _ => None,
290        }
291    }
292
293    /// Handle a mouse click at absolute screen coordinates `(x, y)`.
294    ///
295    /// Returns `Some(LauncherAction::Launch(...))` if an app entry was clicked,
296    /// or `None` if the click was outside the grid area (but still inside the
297    /// overlay).
298    pub fn handle_click(&mut self, x: usize, y: usize) -> Option<LauncherAction> {
299        if self.state == LauncherState::Hidden {
300            return None;
301        }
302
303        // Convert from screen coordinates to overlay-local coordinates
304        let local_x = if x >= self.overlay_x {
305            x - self.overlay_x
306        } else {
307            return None;
308        };
309        let local_y = if y >= self.overlay_y {
310            y - self.overlay_y
311        } else {
312            return None;
313        };
314
315        // Check bounds
316        if local_x >= self.overlay_width || local_y >= self.overlay_height {
317            return None;
318        }
319
320        // Grid layout constants (must match render_to_buffer)
321        let search_bar_height = SEARCH_BAR_HEIGHT;
322        let grid_top = search_bar_height + GRID_PADDING_TOP;
323        let cell_w = self.cell_width();
324        let cell_h = self.cell_height();
325
326        if local_y < grid_top {
327            // Click in search bar area -- activate search mode
328            self.state = LauncherState::SearchActive;
329            return None;
330        }
331
332        let grid_y = local_y - grid_top;
333        let grid_x = if local_x >= GRID_PADDING_LEFT {
334            local_x - GRID_PADDING_LEFT
335        } else {
336            return None;
337        };
338
339        // Determine which cell was clicked
340        let col = grid_x / cell_w;
341        let row = grid_y / cell_h;
342
343        if col >= self.grid_columns {
344            return None;
345        }
346
347        let entry_idx = self.scroll_offset + row * self.grid_columns + col;
348        if entry_idx < self.filtered.len() {
349            self.selected_index = entry_idx;
350            let real_idx = self.filtered[entry_idx];
351            if real_idx < self.entries.len() {
352                let path = self.entries[real_idx].exec_path.clone();
353                self.hide();
354                return Some(LauncherAction::Launch(path));
355            }
356        }
357
358        None
359    }
360
361    /// Filter `entries` by the current `search_query`.
362    ///
363    /// Performs a case-insensitive substring match on the application name.
364    /// If the query is empty, all entries are shown.
365    pub fn filter_entries(&mut self) {
366        self.filtered.clear();
367
368        if self.search_query.is_empty() {
369            for i in 0..self.entries.len() {
370                self.filtered.push(i);
371            }
372            return;
373        }
374
375        // Build a lowercase copy of the query for case-insensitive matching.
376        // We do this manually to avoid pulling in Unicode tables.
377        let query_lower: Vec<u8> = self.search_query.bytes().map(ascii_to_lower).collect();
378
379        for (i, entry) in self.entries.iter().enumerate() {
380            if ascii_contains_lower(entry.name.as_bytes(), &query_lower) {
381                self.filtered.push(i);
382            }
383        }
384    }
385
386    /// Get a reference to the currently selected application entry.
387    pub fn selected_entry(&self) -> Option<&AppEntry> {
388        let idx = self.filtered.get(self.selected_index)?;
389        self.entries.get(*idx)
390    }
391
392    /// Get the indices of visible (filtered) entries.
393    pub fn visible_entries(&self) -> &[usize] {
394        &self.filtered
395    }
396
397    /// Get the total number of filtered entries.
398    pub fn filtered_count(&self) -> usize {
399        self.filtered.len()
400    }
401
402    /// Get a reference to all registered entries.
403    pub fn entries(&self) -> &[AppEntry] {
404        &self.entries
405    }
406
407    /// Render the launcher overlay into a u32 BGRA pixel buffer.
408    ///
409    /// The buffer dimensions must be `buf_width * buf_height` elements.
410    /// The launcher is drawn at its configured overlay position.
411    pub fn render_to_buffer(&self, buffer: &mut [u32], buf_width: usize, buf_height: usize) {
412        if self.state == LauncherState::Hidden {
413            return;
414        }
415
416        let ov_x = self.overlay_x;
417        let ov_y = self.overlay_y;
418        let ov_w = self.overlay_width;
419        let ov_h = self.overlay_height;
420
421        // --- 1. Semi-transparent dark background overlay ---
422        let bg_color: u32 = 0xCC222222; // ~80% opaque dark grey
423        for row in ov_y..(ov_y + ov_h).min(buf_height) {
424            for col in ov_x..(ov_x + ov_w).min(buf_width) {
425                let idx = row * buf_width + col;
426                if idx < buffer.len() {
427                    buffer[idx] = alpha_blend(buffer[idx], bg_color);
428                }
429            }
430        }
431
432        // --- 2. Overlay border (1px lighter line) ---
433        let border_color: u32 = 0xFF555555;
434        // Top edge
435        if ov_y < buf_height {
436            for col in ov_x..(ov_x + ov_w).min(buf_width) {
437                let idx = ov_y * buf_width + col;
438                if idx < buffer.len() {
439                    buffer[idx] = border_color;
440                }
441            }
442        }
443        // Bottom edge
444        let bottom_y = ov_y + ov_h - 1;
445        if bottom_y < buf_height {
446            for col in ov_x..(ov_x + ov_w).min(buf_width) {
447                let idx = bottom_y * buf_width + col;
448                if idx < buffer.len() {
449                    buffer[idx] = border_color;
450                }
451            }
452        }
453        // Left edge
454        for row in ov_y..(ov_y + ov_h).min(buf_height) {
455            let idx = row * buf_width + ov_x;
456            if idx < buffer.len() && ov_x < buf_width {
457                buffer[idx] = border_color;
458            }
459        }
460        // Right edge
461        let right_x = ov_x + ov_w - 1;
462        if right_x < buf_width {
463            for row in ov_y..(ov_y + ov_h).min(buf_height) {
464                let idx = row * buf_width + right_x;
465                if idx < buffer.len() {
466                    buffer[idx] = border_color;
467                }
468            }
469        }
470
471        // --- 3. Search bar ---
472        let search_y = ov_y + SEARCH_BAR_MARGIN_TOP;
473        let search_x = ov_x + SEARCH_BAR_MARGIN_LEFT;
474        let search_w = ov_w - SEARCH_BAR_MARGIN_LEFT * 2;
475        let search_h = SEARCH_BAR_INNER_HEIGHT;
476
477        // Search bar background
478        let search_bg = if self.state == LauncherState::SearchActive {
479            0xFF3A3A3A
480        } else {
481            0xFF333333
482        };
483        for row in search_y..(search_y + search_h).min(buf_height) {
484            for col in search_x..(search_x + search_w).min(buf_width) {
485                let idx = row * buf_width + col;
486                if idx < buffer.len() {
487                    buffer[idx] = search_bg;
488                }
489            }
490        }
491
492        // Search bar border
493        let search_border = if self.state == LauncherState::SearchActive {
494            0xFF6688AA
495        } else {
496            0xFF555555
497        };
498        draw_rect_outline(
499            buffer,
500            buf_width,
501            buf_height,
502            search_x,
503            search_y,
504            search_w,
505            search_h,
506            search_border,
507        );
508
509        // Search text or placeholder
510        let text_y = search_y + (search_h.saturating_sub(FONT_HEIGHT)) / 2;
511        let text_x = search_x + 8;
512        if self.search_query.is_empty() {
513            // Placeholder
514            draw_text_u32(
515                buffer,
516                buf_width,
517                buf_height,
518                b"Search applications...",
519                text_x,
520                text_y,
521                0xFF777777,
522            );
523        } else {
524            draw_text_u32(
525                buffer,
526                buf_width,
527                buf_height,
528                self.search_query.as_bytes(),
529                text_x,
530                text_y,
531                0xFFDDDDDD,
532            );
533            // Cursor (blinking approximation: always show)
534            let cursor_x = text_x + self.search_query.len() * FONT_WIDTH;
535            for row in text_y..(text_y + FONT_HEIGHT).min(buf_height) {
536                let idx = row * buf_width + cursor_x;
537                if idx < buffer.len() && cursor_x < buf_width {
538                    buffer[idx] = 0xFFCCCCCC;
539                }
540            }
541        }
542
543        // --- 4. Application grid ---
544        let grid_top = ov_y + SEARCH_BAR_HEIGHT + GRID_PADDING_TOP;
545        let grid_left = ov_x + GRID_PADDING_LEFT;
546        let cell_w = self.cell_width();
547        let cell_h = self.cell_height();
548
549        let visible_count = self.grid_columns * self.grid_rows;
550        let start = self.scroll_offset;
551        let end = (start + visible_count).min(self.filtered.len());
552
553        for display_idx in start..end {
554            let local_idx = display_idx - start;
555            let col = local_idx % self.grid_columns;
556            let row = local_idx / self.grid_columns;
557
558            let cell_x = grid_left + col * cell_w;
559            let cell_y = grid_top + row * cell_h;
560
561            let real_idx = self.filtered[display_idx];
562            if real_idx >= self.entries.len() {
563                continue;
564            }
565            let entry = &self.entries[real_idx];
566
567            // Highlight selected entry
568            let is_selected = display_idx == self.selected_index;
569            if is_selected {
570                let hl_color: u32 = 0xFF445566;
571                for ry in cell_y..(cell_y + cell_h).min(buf_height) {
572                    for rx in cell_x..(cell_x + cell_w).min(buf_width) {
573                        let idx = ry * buf_width + rx;
574                        if idx < buffer.len() {
575                            buffer[idx] = hl_color;
576                        }
577                    }
578                }
579            }
580
581            // Icon placeholder: a colored rectangle based on category
582            let icon_size = ICON_SIZE;
583            let icon_x = cell_x + (cell_w.saturating_sub(icon_size)) / 2;
584            let icon_y = cell_y + ICON_MARGIN_TOP;
585            let icon_color = category_color(&entry.category);
586
587            for ry in icon_y..(icon_y + icon_size).min(buf_height) {
588                for rx in icon_x..(icon_x + icon_size).min(buf_width) {
589                    let idx = ry * buf_width + rx;
590                    if idx < buffer.len() {
591                        buffer[idx] = icon_color;
592                    }
593                }
594            }
595
596            // Draw a small letter inside the icon (first char of name)
597            if !entry.name.is_empty() {
598                let first_char = entry.name.as_bytes()[0];
599                let char_x = icon_x + (icon_size.saturating_sub(FONT_WIDTH)) / 2;
600                let char_y = icon_y + (icon_size.saturating_sub(FONT_HEIGHT)) / 2;
601                draw_char_u32(
602                    buffer, buf_width, buf_height, first_char, char_x, char_y, 0xFFFFFFFF,
603                );
604            }
605
606            // Application name below the icon (centered, truncated)
607            let name_bytes = entry.name.as_bytes();
608            let max_name_chars = cell_w / FONT_WIDTH;
609            let name_len = name_bytes.len().min(max_name_chars);
610            let name_pixel_w = name_len * FONT_WIDTH;
611            let name_x = cell_x + (cell_w.saturating_sub(name_pixel_w)) / 2;
612            let name_y = icon_y + icon_size + NAME_MARGIN_TOP;
613            let name_color = if is_selected { 0xFFFFFFFF } else { 0xFFCCCCCC };
614            draw_text_u32(
615                buffer,
616                buf_width,
617                buf_height,
618                &name_bytes[..name_len],
619                name_x,
620                name_y,
621                name_color,
622            );
623
624            // Description below the name (smaller, dimmer, single line)
625            if !entry.description.is_empty() {
626                let desc_bytes = entry.description.as_bytes();
627                let max_desc_chars = cell_w / FONT_WIDTH;
628                let desc_len = desc_bytes.len().min(max_desc_chars);
629                let desc_pixel_w = desc_len * FONT_WIDTH;
630                let desc_x = cell_x + (cell_w.saturating_sub(desc_pixel_w)) / 2;
631                let desc_y = name_y + FONT_HEIGHT + 2;
632                draw_text_u32(
633                    buffer,
634                    buf_width,
635                    buf_height,
636                    &desc_bytes[..desc_len],
637                    desc_x,
638                    desc_y,
639                    0xFF888888,
640                );
641            }
642        }
643
644        // --- 5. Scroll indicator ---
645        let total_pages = (self.filtered.len() + visible_count - 1) / visible_count.max(1);
646        if total_pages > 1 {
647            let current_page = self.scroll_offset / visible_count.max(1);
648            // Draw small dots at the bottom center of the overlay
649            let dots_y = ov_y + ov_h - 16;
650            let dots_total_w = total_pages * 12;
651            let dots_x = ov_x + (ov_w.saturating_sub(dots_total_w)) / 2;
652
653            for page in 0..total_pages {
654                let dot_x = dots_x + page * 12 + 2;
655                let dot_color = if page == current_page {
656                    0xFFDDDDDD
657                } else {
658                    0xFF666666
659                };
660                // Draw a 6x6 dot
661                for ry in dots_y..(dots_y + 6).min(buf_height) {
662                    for rx in dot_x..(dot_x + 6).min(buf_width) {
663                        let idx = ry * buf_width + rx;
664                        if idx < buffer.len() {
665                            buffer[idx] = dot_color;
666                        }
667                    }
668                }
669            }
670        }
671
672        // --- 6. Result count indicator ---
673        let count_text = format_count(self.filtered.len(), self.entries.len());
674        let count_y = ov_y + ov_h - 16;
675        let count_x = ov_x + 8;
676        draw_text_u32(
677            buffer,
678            buf_width,
679            buf_height,
680            count_text.as_bytes(),
681            count_x,
682            count_y,
683            0xFF666666,
684        );
685    }
686
687    // -----------------------------------------------------------------------
688    // Internal helpers
689    // -----------------------------------------------------------------------
690
691    /// Width of each grid cell in pixels.
692    fn cell_width(&self) -> usize {
693        let usable = self.overlay_width - GRID_PADDING_LEFT * 2;
694        usable / self.grid_columns.max(1)
695    }
696
697    /// Height of each grid cell in pixels.
698    fn cell_height(&self) -> usize {
699        let grid_area_h =
700            self.overlay_height - SEARCH_BAR_HEIGHT - GRID_PADDING_TOP - GRID_PADDING_BOTTOM;
701        grid_area_h / self.grid_rows.max(1)
702    }
703
704    /// Clamp `selected_index` to the valid range after filtering.
705    fn clamp_selection(&mut self) {
706        if self.filtered.is_empty() {
707            self.selected_index = 0;
708        } else if self.selected_index >= self.filtered.len() {
709            self.selected_index = self.filtered.len() - 1;
710        }
711    }
712
713    /// Adjust `scroll_offset` so the `selected_index` is within the visible
714    /// grid page.
715    fn ensure_visible(&mut self) {
716        let page_size = self.grid_columns * self.grid_rows;
717        if page_size == 0 {
718            return;
719        }
720
721        // Scroll down if selection is below visible range
722        while self.selected_index >= self.scroll_offset + page_size {
723            self.scroll_offset += self.grid_columns;
724        }
725        // Scroll up if selection is above visible range
726        while self.selected_index < self.scroll_offset && self.scroll_offset > 0 {
727            self.scroll_offset = self.scroll_offset.saturating_sub(self.grid_columns);
728        }
729    }
730}
731
732impl Default for AppLauncher {
733    fn default() -> Self {
734        Self::new()
735    }
736}
737
738// ---------------------------------------------------------------------------
739// Layout constants
740// ---------------------------------------------------------------------------
741
742/// Font glyph width in pixels (8x16 VGA font).
743const FONT_WIDTH: usize = 8;
744/// Font glyph height in pixels (8x16 VGA font).
745const FONT_HEIGHT: usize = 16;
746
747/// Total height reserved for the search bar area (including margins).
748const SEARCH_BAR_HEIGHT: usize = 48;
749/// Top margin above the search bar input field.
750const SEARCH_BAR_MARGIN_TOP: usize = 12;
751/// Left/right margin of the search bar input field.
752const SEARCH_BAR_MARGIN_LEFT: usize = 16;
753/// Inner height of the search bar input field.
754const SEARCH_BAR_INNER_HEIGHT: usize = 28;
755
756/// Top padding between search bar and grid area.
757const GRID_PADDING_TOP: usize = 12;
758/// Bottom padding below the grid area.
759const GRID_PADDING_BOTTOM: usize = 24;
760/// Left padding before the first grid column.
761const GRID_PADDING_LEFT: usize = 16;
762
763/// Icon placeholder size in pixels (square).
764const ICON_SIZE: usize = 48;
765/// Margin above the icon within a grid cell.
766const ICON_MARGIN_TOP: usize = 8;
767/// Margin between the icon and the application name text.
768const NAME_MARGIN_TOP: usize = 4;
769
770// ---------------------------------------------------------------------------
771// .desktop file parser
772// ---------------------------------------------------------------------------
773
774/// Parse a freedesktop .desktop file and extract an `AppEntry`.
775///
776/// Supports the `[Desktop Entry]` section and the following keys:
777/// - `Name=` -- application name
778/// - `Exec=` -- executable path (first token only, `%f/%u/%F/%U` stripped)
779/// - `Icon=` -- icon name
780/// - `Comment=` -- short description
781/// - `Categories=` -- semicolon-separated category list (first recognized
782///   category is used)
783///
784/// Returns `None` if `Name` or `Exec` is missing.
785pub fn parse_desktop_file(content: &str) -> Option<AppEntry> {
786    let mut name: Option<&str> = None;
787    let mut exec: Option<&str> = None;
788    let mut icon: Option<&str> = None;
789    let mut comment: Option<&str> = None;
790    let mut categories_raw: Option<&str> = None;
791    let mut in_desktop_entry = false;
792
793    for line in content.lines() {
794        let trimmed = line.trim();
795
796        // Section header
797        if trimmed.starts_with('[') {
798            in_desktop_entry = trimmed == "[Desktop Entry]";
799            continue;
800        }
801
802        if !in_desktop_entry {
803            continue;
804        }
805
806        // Skip comments
807        if trimmed.starts_with('#') {
808            continue;
809        }
810
811        if let Some(val) = strip_key(trimmed, "Name=") {
812            name = Some(val);
813        } else if let Some(val) = strip_key(trimmed, "Exec=") {
814            exec = Some(val);
815        } else if let Some(val) = strip_key(trimmed, "Icon=") {
816            icon = Some(val);
817        } else if let Some(val) = strip_key(trimmed, "Comment=") {
818            comment = Some(val);
819        } else if let Some(val) = strip_key(trimmed, "Categories=") {
820            categories_raw = Some(val);
821        }
822    }
823
824    let name_str = name?;
825    let exec_str = exec?;
826
827    // Strip field codes (%f, %u, %F, %U, etc.) from exec path
828    let exec_clean = strip_field_codes(exec_str);
829
830    let category = categories_raw
831        .and_then(parse_category_string)
832        .unwrap_or(AppCategory::Other);
833
834    Some(AppEntry {
835        name: String::from(name_str),
836        exec_path: exec_clean,
837        icon_name: String::from(icon.unwrap_or("")),
838        category,
839        description: String::from(comment.unwrap_or("")),
840    })
841}
842
843/// Strip a key prefix from a line and return the value portion.
844fn strip_key<'a>(line: &'a str, key: &str) -> Option<&'a str> {
845    line.strip_prefix(key).map(|s| s.trim())
846}
847
848/// Strip freedesktop field codes (%f, %u, etc.) from an Exec value.
849fn strip_field_codes(exec: &str) -> String {
850    let mut result = String::with_capacity(exec.len());
851    let bytes = exec.as_bytes();
852    let mut i = 0;
853    while i < bytes.len() {
854        if bytes[i] == b'%' && i + 1 < bytes.len() {
855            // Skip the % and the following character
856            i += 2;
857            // Skip any trailing space after the field code
858            if i < bytes.len() && bytes[i] == b' ' {
859                i += 1;
860            }
861        } else {
862            result.push(bytes[i] as char);
863            i += 1;
864        }
865    }
866    // Trim trailing whitespace
867    while result.ends_with(' ') {
868        result.pop();
869    }
870    result
871}
872
873/// Parse a semicolon-separated categories string and return the first
874/// recognized `AppCategory`.
875fn parse_category_string(cats: &str) -> Option<AppCategory> {
876    for segment in cats.split(';') {
877        let cat = segment.trim();
878        if cat.is_empty() {
879            continue;
880        }
881        match cat {
882            "System" | "Monitor" | "PackageManager" => return Some(AppCategory::System),
883            "Utility" | "Accessibility" | "Calculator" | "Clock" => {
884                return Some(AppCategory::Utility)
885            }
886            "Development" | "IDE" | "TextEditor" | "Debugger" | "WebDevelopment" => {
887                return Some(AppCategory::Development)
888            }
889            "Graphics" | "2DGraphics" | "3DGraphics" | "RasterGraphics" | "VectorGraphics" => {
890                return Some(AppCategory::Graphics)
891            }
892            "Network" | "WebBrowser" | "Email" | "Chat" | "IRCClient" | "FileTransfer" => {
893                return Some(AppCategory::Network)
894            }
895            "AudioVideo" | "Audio" | "Video" | "Multimedia" | "Player" | "Recorder" => {
896                return Some(AppCategory::Multimedia)
897            }
898            "Office" | "WordProcessor" | "Spreadsheet" | "Presentation" => {
899                return Some(AppCategory::Office)
900            }
901            "Settings" | "Preferences" | "DesktopSettings" | "HardwareSettings" => {
902                return Some(AppCategory::Settings)
903            }
904            _ => {}
905        }
906    }
907    None
908}
909
910// ---------------------------------------------------------------------------
911// Default applications
912// ---------------------------------------------------------------------------
913
914/// Build the list of built-in default applications.
915fn default_applications() -> Vec<AppEntry> {
916    vec![
917        AppEntry::new(
918            "Terminal",
919            "/usr/bin/terminal",
920            "utilities-terminal",
921            AppCategory::System,
922            "Terminal emulator",
923        ),
924        AppEntry::new(
925            "File Manager",
926            "/usr/bin/files",
927            "system-file-manager",
928            AppCategory::System,
929            "Browse files",
930        ),
931        AppEntry::new(
932            "Text Editor",
933            "/usr/bin/editor",
934            "accessories-text-editor",
935            AppCategory::Utility,
936            "Edit text files",
937        ),
938        AppEntry::new(
939            "Settings",
940            "/usr/bin/settings",
941            "preferences-system",
942            AppCategory::Settings,
943            "System settings",
944        ),
945        AppEntry::new(
946            "System Monitor",
947            "/usr/bin/sysmonitor",
948            "utilities-system-monitor",
949            AppCategory::System,
950            "Monitor CPU and memory",
951        ),
952        AppEntry::new(
953            "Image Viewer",
954            "/usr/bin/image-viewer",
955            "eog",
956            AppCategory::Graphics,
957            "View images",
958        ),
959        AppEntry::new(
960            "Media Player",
961            "/usr/bin/mediaplayer",
962            "multimedia-player",
963            AppCategory::Multimedia,
964            "Play audio and video",
965        ),
966        AppEntry::new(
967            "Web Browser",
968            "/usr/bin/browser",
969            "web-browser",
970            AppCategory::Network,
971            "Browse the web",
972        ),
973        AppEntry::new(
974            "PDF Viewer",
975            "/usr/bin/pdfviewer",
976            "pdf-viewer",
977            AppCategory::Utility,
978            "View PDF documents",
979        ),
980    ]
981}
982
983/// Return a BGRA color for a category icon placeholder.
984///
985/// Each category gets a distinctive color so users can quickly identify
986/// application types at a glance.
987pub fn category_color(cat: &AppCategory) -> u32 {
988    match cat {
989        AppCategory::System => 0xFF4488AA,      // Teal
990        AppCategory::Utility => 0xFF66AA44,     // Green
991        AppCategory::Development => 0xFF886644, // Brown/Amber
992        AppCategory::Graphics => 0xFFAA6688,    // Rose
993        AppCategory::Network => 0xFF4466AA,     // Blue
994        AppCategory::Multimedia => 0xFFAA4466,  // Magenta
995        AppCategory::Office => 0xFF666699,      // Slate blue
996        AppCategory::Settings => 0xFF888888,    // Grey
997        AppCategory::Other => 0xFF555555,       // Dark grey
998    }
999}
1000
1001// ---------------------------------------------------------------------------
1002// Drawing helpers (u32 pixel buffer)
1003// ---------------------------------------------------------------------------
1004
1005/// Draw a single 8x16 character into a u32 (BGRA/XRGB) pixel buffer.
1006fn draw_char_u32(
1007    buffer: &mut [u32],
1008    buf_width: usize,
1009    buf_height: usize,
1010    ch: u8,
1011    px: usize,
1012    py: usize,
1013    color: u32,
1014) {
1015    let glyph = crate::graphics::font8x16::glyph(ch);
1016    for (row, &bits) in glyph.iter().enumerate() {
1017        let y = py + row;
1018        if y >= buf_height {
1019            break;
1020        }
1021        for col in 0..8 {
1022            if (bits >> (7 - col)) & 1 != 0 {
1023                let x = px + col;
1024                if x >= buf_width {
1025                    break;
1026                }
1027                let idx = y * buf_width + x;
1028                if idx < buffer.len() {
1029                    buffer[idx] = color;
1030                }
1031            }
1032        }
1033    }
1034}
1035
1036/// Draw a byte string into a u32 pixel buffer using the 8x16 VGA font.
1037fn draw_text_u32(
1038    buffer: &mut [u32],
1039    buf_width: usize,
1040    buf_height: usize,
1041    text: &[u8],
1042    px: usize,
1043    py: usize,
1044    color: u32,
1045) {
1046    for (i, &ch) in text.iter().enumerate() {
1047        draw_char_u32(
1048            buffer,
1049            buf_width,
1050            buf_height,
1051            ch,
1052            px + i * FONT_WIDTH,
1053            py,
1054            color,
1055        );
1056    }
1057}
1058
1059/// Draw a 1px rectangle outline into a u32 pixel buffer.
1060fn draw_rect_outline(
1061    buffer: &mut [u32],
1062    buf_width: usize,
1063    buf_height: usize,
1064    x: usize,
1065    y: usize,
1066    w: usize,
1067    h: usize,
1068    color: u32,
1069) {
1070    // Top and bottom edges
1071    for col in x..(x + w).min(buf_width) {
1072        if y < buf_height {
1073            let idx = y * buf_width + col;
1074            if idx < buffer.len() {
1075                buffer[idx] = color;
1076            }
1077        }
1078        let bottom = y + h - 1;
1079        if bottom < buf_height {
1080            let idx = bottom * buf_width + col;
1081            if idx < buffer.len() {
1082                buffer[idx] = color;
1083            }
1084        }
1085    }
1086    // Left and right edges
1087    for row in y..(y + h).min(buf_height) {
1088        if x < buf_width {
1089            let idx = row * buf_width + x;
1090            if idx < buffer.len() {
1091                buffer[idx] = color;
1092            }
1093        }
1094        let right = x + w - 1;
1095        if right < buf_width {
1096            let idx = row * buf_width + right;
1097            if idx < buffer.len() {
1098                buffer[idx] = color;
1099            }
1100        }
1101    }
1102}
1103
1104/// Simple alpha blending of a foreground pixel over a background pixel.
1105///
1106/// Both pixels are in 0xAARRGGBB format. Uses integer-only arithmetic.
1107fn alpha_blend(bg: u32, fg: u32) -> u32 {
1108    let fg_a = (fg >> 24) & 0xFF;
1109    if fg_a == 0xFF {
1110        return fg;
1111    }
1112    if fg_a == 0 {
1113        return bg;
1114    }
1115
1116    let inv_a = 255 - fg_a;
1117
1118    let fg_r = (fg >> 16) & 0xFF;
1119    let fg_g = (fg >> 8) & 0xFF;
1120    let fg_b = fg & 0xFF;
1121
1122    let bg_r = (bg >> 16) & 0xFF;
1123    let bg_g = (bg >> 8) & 0xFF;
1124    let bg_b = bg & 0xFF;
1125
1126    // out = fg * alpha + bg * (1 - alpha), with integer division by 255
1127    let r = (fg_r * fg_a + bg_r * inv_a) / 255;
1128    let g = (fg_g * fg_a + bg_g * inv_a) / 255;
1129    let b = (fg_b * fg_a + bg_b * inv_a) / 255;
1130
1131    0xFF000000 | (r << 16) | (g << 8) | b
1132}
1133
1134// ---------------------------------------------------------------------------
1135// ASCII utility helpers
1136// ---------------------------------------------------------------------------
1137
1138/// Convert an ASCII byte to lowercase (identity for non-alpha bytes).
1139fn ascii_to_lower(b: u8) -> u8 {
1140    if b.is_ascii_uppercase() {
1141        b + 32
1142    } else {
1143        b
1144    }
1145}
1146
1147/// Case-insensitive check whether `haystack` contains the already-lowered
1148/// `needle` as a substring.
1149fn ascii_contains_lower(haystack: &[u8], needle: &[u8]) -> bool {
1150    if needle.is_empty() {
1151        return true;
1152    }
1153    if needle.len() > haystack.len() {
1154        return false;
1155    }
1156    let limit = haystack.len() - needle.len();
1157    for start in 0..=limit {
1158        let mut matches = true;
1159        for (j, &nb) in needle.iter().enumerate() {
1160            if ascii_to_lower(haystack[start + j]) != nb {
1161                matches = false;
1162                break;
1163            }
1164        }
1165        if matches {
1166            return true;
1167        }
1168    }
1169    false
1170}
1171
1172/// Format a "N of M apps" count string without pulling in `format!`.
1173fn format_count(filtered: usize, total: usize) -> String {
1174    let mut s = String::with_capacity(32);
1175    append_usize(&mut s, filtered);
1176    s.push_str(" of ");
1177    append_usize(&mut s, total);
1178    s.push_str(" apps");
1179    s
1180}
1181
1182/// Append a `usize` as decimal digits to a `String`.
1183fn append_usize(s: &mut String, n: usize) {
1184    if n == 0 {
1185        s.push('0');
1186        return;
1187    }
1188    // Max digits for usize on 64-bit is 20
1189    let mut buf = [0u8; 20];
1190    let mut pos = buf.len();
1191    let mut val = n;
1192    while val > 0 {
1193        pos -= 1;
1194        buf[pos] = b'0' + (val % 10) as u8;
1195        val /= 10;
1196    }
1197    for &ch in &buf[pos..] {
1198        s.push(ch as char);
1199    }
1200}
1201
1202// ---------------------------------------------------------------------------
1203// Global state
1204// ---------------------------------------------------------------------------
1205
1206/// Global application launcher instance.
1207static LAUNCHER: GlobalState<spin::Mutex<AppLauncher>> = GlobalState::new();
1208
1209/// Initialize the application launcher.
1210pub fn init() -> Result<(), crate::error::KernelError> {
1211    LAUNCHER
1212        .init(spin::Mutex::new(AppLauncher::new()))
1213        .map_err(|_| crate::error::KernelError::InvalidState {
1214            expected: "uninitialized",
1215            actual: "initialized",
1216        })?;
1217
1218    crate::println!("[LAUNCHER] Application launcher initialized ({} apps)", 7);
1219    Ok(())
1220}
1221
1222/// Execute a function with the application launcher (mutable access).
1223pub fn with_launcher<R, F: FnOnce(&mut AppLauncher) -> R>(f: F) -> Option<R> {
1224    LAUNCHER.with(|lock| {
1225        let mut launcher = lock.lock();
1226        f(&mut launcher)
1227    })
1228}
1229
1230/// Execute a function with the application launcher (read-only access).
1231pub fn with_launcher_ref<R, F: FnOnce(&AppLauncher) -> R>(f: F) -> Option<R> {
1232    LAUNCHER.with(|lock| {
1233        let launcher = lock.lock();
1234        f(&launcher)
1235    })
1236}