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

veridian_kernel/desktop/
settings.rs

1//! System Settings Application
2//!
3//! Provides a settings interface for configuring display, network,
4//! users, and appearance preferences.
5
6#![allow(dead_code)]
7
8use alloc::{format, string::String, vec, vec::Vec};
9
10use super::renderer::draw_string_into_buffer;
11
12// ---------------------------------------------------------------------------
13// Settings panel categories
14// ---------------------------------------------------------------------------
15
16/// Which panel is active in the settings sidebar.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SettingsPanel {
19    Display,
20    Network,
21    Users,
22    Appearance,
23    Audio,
24    Bluetooth,
25    Power,
26    About,
27}
28
29impl SettingsPanel {
30    /// Label shown in the sidebar for this panel.
31    pub fn label(&self) -> &'static str {
32        match self {
33            Self::Display => "Display",
34            Self::Network => "Network",
35            Self::Users => "Users",
36            Self::Appearance => "Appearance",
37            Self::Audio => "Audio",
38            Self::Bluetooth => "Bluetooth",
39            Self::Power => "Power",
40            Self::About => "About",
41        }
42    }
43
44    /// Ordered list of all panels.
45    pub fn all() -> &'static [SettingsPanel] {
46        &[
47            Self::Display,
48            Self::Network,
49            Self::Users,
50            Self::Appearance,
51            Self::Audio,
52            Self::Bluetooth,
53            Self::Power,
54            Self::About,
55        ]
56    }
57
58    /// Index of this panel in the ordered list.
59    fn index(&self) -> usize {
60        match self {
61            Self::Display => 0,
62            Self::Network => 1,
63            Self::Users => 2,
64            Self::Appearance => 3,
65            Self::Audio => 4,
66            Self::Bluetooth => 5,
67            Self::Power => 6,
68            Self::About => 7,
69        }
70    }
71}
72
73// ---------------------------------------------------------------------------
74// Per-panel settings structs
75// ---------------------------------------------------------------------------
76
77/// Display-related settings.
78#[derive(Debug, Clone)]
79pub struct DisplaySettings {
80    pub resolution_index: usize,
81    pub brightness: u8,
82    pub available_resolutions: Vec<(usize, usize)>,
83}
84
85impl Default for DisplaySettings {
86    fn default() -> Self {
87        Self {
88            resolution_index: 0,
89            brightness: 80,
90            available_resolutions: vec![(1280, 800), (1024, 768), (800, 600), (1920, 1080)],
91        }
92    }
93}
94
95impl DisplaySettings {
96    /// Number of configurable items in this panel.
97    fn item_count(&self) -> usize {
98        2 // resolution, brightness
99    }
100}
101
102/// Network-related settings.
103#[derive(Debug, Clone)]
104pub struct NetworkSettings {
105    pub hostname: String,
106    pub dhcp_enabled: bool,
107    pub ip_address: String,
108    pub gateway: String,
109    pub dns: String,
110}
111
112impl Default for NetworkSettings {
113    fn default() -> Self {
114        Self {
115            hostname: String::from("veridian"),
116            dhcp_enabled: true,
117            ip_address: String::from("10.0.2.15"),
118            gateway: String::from("10.0.2.2"),
119            dns: String::from("10.0.2.3"),
120        }
121    }
122}
123
124impl NetworkSettings {
125    fn item_count(&self) -> usize {
126        5 // hostname, dhcp, ip, gateway, dns
127    }
128}
129
130/// User account settings.
131#[derive(Debug, Clone)]
132pub struct UserSettings {
133    pub username: String,
134    pub shell: String,
135    pub home_dir: String,
136}
137
138impl Default for UserSettings {
139    fn default() -> Self {
140        Self {
141            username: String::from("root"),
142            shell: String::from("/bin/vsh"),
143            home_dir: String::from("/root"),
144        }
145    }
146}
147
148impl UserSettings {
149    fn item_count(&self) -> usize {
150        3 // username, shell, home_dir
151    }
152}
153
154/// Panel position on screen.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PanelPosition {
157    Top,
158    Bottom,
159}
160
161/// Appearance settings.
162#[derive(Debug, Clone)]
163pub struct AppearanceSettings {
164    pub theme_index: usize,
165    pub font_size: u8,
166    pub show_desktop_icons: bool,
167    pub panel_position: PanelPosition,
168}
169
170impl Default for AppearanceSettings {
171    fn default() -> Self {
172        Self {
173            theme_index: 0,
174            font_size: 16,
175            show_desktop_icons: true,
176            panel_position: PanelPosition::Bottom,
177        }
178    }
179}
180
181impl AppearanceSettings {
182    fn item_count(&self) -> usize {
183        4 // theme, font_size, show_icons, panel_position
184    }
185}
186
187/// Audio settings.
188#[derive(Debug, Clone)]
189pub struct AudioSettings {
190    pub master_volume: u8,
191    pub muted: bool,
192    pub output_device: String,
193}
194
195impl Default for AudioSettings {
196    fn default() -> Self {
197        Self {
198            master_volume: 75,
199            muted: false,
200            output_device: String::from("Default Output"),
201        }
202    }
203}
204
205impl AudioSettings {
206    fn item_count(&self) -> usize {
207        3 // volume, mute, output device
208    }
209}
210
211/// Bluetooth settings.
212#[derive(Debug, Clone)]
213pub struct BluetoothSettings {
214    pub enabled: bool,
215    pub discoverable: bool,
216    pub device_name: String,
217}
218
219impl Default for BluetoothSettings {
220    fn default() -> Self {
221        Self {
222            enabled: false,
223            discoverable: false,
224            device_name: String::from("VeridianOS"),
225        }
226    }
227}
228
229impl BluetoothSettings {
230    fn item_count(&self) -> usize {
231        3 // enabled, discoverable, device name
232    }
233}
234
235/// Power settings.
236#[derive(Debug, Clone)]
237pub struct PowerSettings {
238    pub profile_index: usize,
239    pub screen_timeout_min: u8,
240    pub profiles: Vec<&'static str>,
241}
242
243impl Default for PowerSettings {
244    fn default() -> Self {
245        Self {
246            profile_index: 0,
247            screen_timeout_min: 10,
248            profiles: vec!["Balanced", "Performance", "Power Saver"],
249        }
250    }
251}
252
253impl PowerSettings {
254    fn item_count(&self) -> usize {
255        2 // profile, screen timeout
256    }
257}
258
259/// Static system information displayed on the About panel.
260#[derive(Debug, Clone)]
261pub struct AboutInfo {
262    pub os_name: &'static str,
263    pub version: &'static str,
264    pub kernel_version: &'static str,
265    pub arch: &'static str,
266    pub hostname: String,
267}
268
269impl Default for AboutInfo {
270    fn default() -> Self {
271        Self {
272            os_name: "VeridianOS",
273            version: "0.25.2",
274            kernel_version: "0.25.2-gui",
275            arch: core::env!("CARGO_PKG_NAME"), // will be "veridian-kernel"
276            hostname: String::from("veridian"),
277        }
278    }
279}
280
281// ---------------------------------------------------------------------------
282// Actions returned by input handlers
283// ---------------------------------------------------------------------------
284
285/// Action produced by settings interaction.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub enum SettingsAction {
288    /// No-op.
289    None,
290    /// Close the settings window.
291    Close,
292    /// Apply current settings.
293    Apply,
294    /// Switch to a different panel.
295    SwitchPanel(SettingsPanel),
296}
297
298// ---------------------------------------------------------------------------
299// Main application struct
300// ---------------------------------------------------------------------------
301
302/// System Settings application state.
303pub struct SettingsApp {
304    /// Currently active panel.
305    pub active_panel: SettingsPanel,
306
307    /// Per-panel state.
308    pub display: DisplaySettings,
309    pub network: NetworkSettings,
310    pub user: UserSettings,
311    pub appearance: AppearanceSettings,
312    pub about: AboutInfo,
313    pub audio: AudioSettings,
314    pub bluetooth: BluetoothSettings,
315    pub power_settings: PowerSettings,
316
317    /// Index of the selected item within the active panel's content area.
318    pub selected_item: usize,
319
320    /// Compositor surface ID (set when wired to the desktop).
321    pub surface_id: Option<u32>,
322
323    /// Window dimensions.
324    pub width: usize,
325    pub height: usize,
326}
327
328// Layout constants
329const SIDEBAR_WIDTH: usize = 140;
330const SIDEBAR_ITEM_HEIGHT: usize = 24;
331const CONTENT_X: usize = SIDEBAR_WIDTH + 12;
332const CONTENT_Y: usize = 12;
333const LINE_HEIGHT: usize = 22;
334const CHAR_W: usize = 8;
335
336impl SettingsApp {
337    /// Create a new settings application with default values.
338    pub fn new() -> Self {
339        let mut app = Self {
340            active_panel: SettingsPanel::Display,
341            display: DisplaySettings::default(),
342            network: NetworkSettings::default(),
343            user: UserSettings::default(),
344            appearance: AppearanceSettings::default(),
345            about: AboutInfo::default(),
346            audio: AudioSettings::default(),
347            bluetooth: BluetoothSettings::default(),
348            power_settings: PowerSettings::default(),
349            selected_item: 0,
350            surface_id: None,
351            width: 600,
352            height: 400,
353        };
354        app.refresh_from_kernel();
355        app
356    }
357
358    /// Refresh settings fields from live kernel subsystem state.
359    pub fn refresh_from_kernel(&mut self) {
360        // -- Network: pull IP configuration from the kernel IP stack --
361        let iface = crate::net::ip::get_interface_config();
362        let ip = iface.ip_addr.0;
363        self.network.ip_address = format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]);
364        if let Some(gw) = iface.gateway {
365            self.network.gateway = format!("{}.{}.{}.{}", gw.0[0], gw.0[1], gw.0[2], gw.0[3]);
366        }
367        // DNS is not part of InterfaceConfig; leave default
368
369        // -- Audio: read master volume from the mixer --
370        if let Ok(raw_vol) =
371            crate::audio::mixer::with_mixer(|m: &mut crate::audio::mixer::AudioMixer| {
372                m.get_master_volume()
373            })
374        {
375            // Map 0..65535 -> 0..100
376            self.audio.master_volume = ((raw_vol as u32 * 100) / 65535) as u8;
377        }
378
379        // -- Power: map governor to profile index --
380        let governor = crate::power::get_governor();
381        self.power_settings.profile_index = match governor {
382            crate::power::Governor::OnDemand => 0,    // "Balanced"
383            crate::power::Governor::Performance => 1, // "Performance"
384            crate::power::Governor::PowerSave => 2,   // "Power Saver"
385        };
386
387        // -- Users: look up uid 0 from the user database --
388        let db = crate::syscall::userland_ext::UserDatabase::new();
389        if let Some(entry) = db.get_user_by_uid(0) {
390            self.user.username = entry.username.clone();
391            self.user.home_dir = entry.home.clone();
392            self.user.shell = entry.shell.clone();
393        }
394    }
395
396    /// Switch the active panel, resetting the item selection.
397    pub fn switch_panel(&mut self, panel: SettingsPanel) {
398        self.active_panel = panel;
399        self.selected_item = 0;
400    }
401
402    /// Number of selectable items in the current panel.
403    fn current_item_count(&self) -> usize {
404        match self.active_panel {
405            SettingsPanel::Display => self.display.item_count(),
406            SettingsPanel::Network => self.network.item_count(),
407            SettingsPanel::Users => self.user.item_count(),
408            SettingsPanel::Appearance => self.appearance.item_count(),
409            SettingsPanel::Audio => self.audio.item_count(),
410            SettingsPanel::Bluetooth => self.bluetooth.item_count(),
411            SettingsPanel::Power => self.power_settings.item_count(),
412            SettingsPanel::About => 0, // read-only
413        }
414    }
415
416    /// Handle a keyboard event and return the resulting action.
417    pub fn handle_key(&mut self, key: u8) -> SettingsAction {
418        match key {
419            // Tab -- cycle to next panel
420            b'\t' => {
421                let panels = SettingsPanel::all();
422                let next = (self.active_panel.index() + 1) % panels.len();
423                let panel = panels[next];
424                self.switch_panel(panel);
425                SettingsAction::SwitchPanel(panel)
426            }
427            // Up arrow (scancode translated to 'A' by ANSI CSI in our shell, or raw 72)
428            b'k' | b'K' => {
429                if self.selected_item > 0 {
430                    self.selected_item -= 1;
431                }
432                SettingsAction::None
433            }
434            // Down arrow
435            b'j' | b'J' => {
436                let max = self.current_item_count();
437                if max > 0 && self.selected_item < max - 1 {
438                    self.selected_item += 1;
439                }
440                SettingsAction::None
441            }
442            // Enter -- toggle / activate selected item
443            b'\r' | b'\n' => {
444                self.activate_selected();
445                SettingsAction::Apply
446            }
447            // Escape -- close
448            0x1B => SettingsAction::Close,
449            _ => SettingsAction::None,
450        }
451    }
452
453    /// Handle a mouse click and return the resulting action.
454    pub fn handle_click(&mut self, x: usize, y: usize) -> SettingsAction {
455        // Sidebar click?
456        if x < SIDEBAR_WIDTH {
457            let panels = SettingsPanel::all();
458            let index = y / SIDEBAR_ITEM_HEIGHT;
459            if index < panels.len() {
460                let panel = panels[index];
461                self.switch_panel(panel);
462                return SettingsAction::SwitchPanel(panel);
463            }
464            return SettingsAction::None;
465        }
466
467        // Content area click
468        if x >= CONTENT_X && y >= CONTENT_Y {
469            let item = (y - CONTENT_Y) / LINE_HEIGHT;
470            let max = self.current_item_count();
471            if item < max {
472                self.selected_item = item;
473                self.activate_selected();
474                return SettingsAction::Apply;
475            }
476        }
477
478        SettingsAction::None
479    }
480
481    /// Toggle or cycle the currently selected item.
482    fn activate_selected(&mut self) {
483        match self.active_panel {
484            SettingsPanel::Display => match self.selected_item {
485                0 => {
486                    // Cycle resolution
487                    let n = self.display.available_resolutions.len();
488                    if n > 0 {
489                        self.display.resolution_index = (self.display.resolution_index + 1) % n;
490                    }
491                }
492                1 => {
493                    // Increase brightness by 10, wrap at 100
494                    self.display.brightness = if self.display.brightness >= 100 {
495                        10
496                    } else {
497                        self.display.brightness + 10
498                    };
499                }
500                _ => {}
501            },
502            SettingsPanel::Network => {
503                if self.selected_item == 1 {
504                    // Toggle DHCP
505                    self.network.dhcp_enabled = !self.network.dhcp_enabled;
506                }
507                // Other items would open an edit dialog (future).
508            }
509            SettingsPanel::Users => {
510                // Read-only for now.
511            }
512            SettingsPanel::Appearance => match self.selected_item {
513                0 => {
514                    // Cycle theme (0=dark, 1=light, 2=solarized-dark, 3=solarized-light, 4=nord,
515                    // 5=dracula)
516                    self.appearance.theme_index = (self.appearance.theme_index + 1) % 6;
517                }
518                1 => {
519                    // Cycle font size 12..20
520                    self.appearance.font_size = if self.appearance.font_size >= 20 {
521                        12
522                    } else {
523                        self.appearance.font_size + 2
524                    };
525                }
526                2 => {
527                    self.appearance.show_desktop_icons = !self.appearance.show_desktop_icons;
528                }
529                3 => {
530                    self.appearance.panel_position = match self.appearance.panel_position {
531                        PanelPosition::Top => PanelPosition::Bottom,
532                        PanelPosition::Bottom => PanelPosition::Top,
533                    };
534                }
535                _ => {}
536            },
537            SettingsPanel::Audio => match self.selected_item {
538                0 => {
539                    // Cycle volume by 10
540                    self.audio.master_volume = if self.audio.master_volume >= 100 {
541                        0
542                    } else {
543                        self.audio.master_volume + 10
544                    };
545                    // Apply to kernel mixer
546                    let raw_vol = ((self.audio.master_volume as u32 * 65535) / 100) as u16;
547                    let _ = crate::audio::mixer::with_mixer(
548                        |m: &mut crate::audio::mixer::AudioMixer| {
549                            m.set_master_volume(raw_vol);
550                        },
551                    );
552                }
553                1 => {
554                    self.audio.muted = !self.audio.muted;
555                }
556                _ => {}
557            },
558            SettingsPanel::Bluetooth => match self.selected_item {
559                0 => {
560                    self.bluetooth.enabled = !self.bluetooth.enabled;
561                }
562                1 => {
563                    self.bluetooth.discoverable = !self.bluetooth.discoverable;
564                }
565                _ => {}
566            },
567            SettingsPanel::Power => match self.selected_item {
568                0 => {
569                    let n = self.power_settings.profiles.len();
570                    if n > 0 {
571                        self.power_settings.profile_index =
572                            (self.power_settings.profile_index + 1) % n;
573                    }
574                    // Write back to kernel
575                    let gov = match self.power_settings.profile_index {
576                        0 => crate::power::Governor::OnDemand,
577                        1 => crate::power::Governor::Performance,
578                        _ => crate::power::Governor::PowerSave,
579                    };
580                    let _ = crate::power::set_governor(gov);
581                }
582                1 => {
583                    // Cycle screen timeout: 5, 10, 15, 30, 0(never)
584                    self.power_settings.screen_timeout_min =
585                        match self.power_settings.screen_timeout_min {
586                            5 => 10,
587                            10 => 15,
588                            15 => 30,
589                            30 => 0,
590                            _ => 5,
591                        };
592                }
593                _ => {}
594            },
595            SettingsPanel::About => {} // read-only
596        }
597    }
598
599    // -----------------------------------------------------------------------
600    // Rendering
601    // -----------------------------------------------------------------------
602
603    /// Render the settings UI directly into a BGRA `u8` pixel buffer.
604    ///
605    /// `buf` must be at least `buf_width * buf_height * 4` bytes.
606    pub fn render_to_u8_buffer(&self, buf: &mut [u8], buf_width: usize, buf_height: usize) {
607        // -- background (dark gray) --
608        for chunk in buf.chunks_exact_mut(4) {
609            chunk[0] = 0x28; // B
610            chunk[1] = 0x28; // G
611            chunk[2] = 0x28; // R
612            chunk[3] = 0xFF; // A
613        }
614
615        // -- sidebar background (slightly lighter) --
616        for y in 0..buf_height {
617            for x in 0..SIDEBAR_WIDTH.min(buf_width) {
618                let off = (y * buf_width + x) * 4;
619                if off + 3 < buf.len() {
620                    buf[off] = 0x32;
621                    buf[off + 1] = 0x32;
622                    buf[off + 2] = 0x32;
623                    buf[off + 3] = 0xFF;
624                }
625            }
626        }
627
628        // -- sidebar panel items --
629        let panels = SettingsPanel::all();
630        for (i, panel) in panels.iter().enumerate() {
631            let item_y = i * SIDEBAR_ITEM_HEIGHT;
632
633            // Highlight active panel
634            if *panel == self.active_panel {
635                for dy in 0..SIDEBAR_ITEM_HEIGHT {
636                    for x in 0..SIDEBAR_WIDTH.min(buf_width) {
637                        let off = ((item_y + dy) * buf_width + x) * 4;
638                        if off + 3 < buf.len() {
639                            buf[off] = 0x55;
640                            buf[off + 1] = 0x44;
641                            buf[off + 2] = 0x33;
642                            buf[off + 3] = 0xFF;
643                        }
644                    }
645                }
646            }
647
648            let color = if *panel == self.active_panel {
649                0xFFFFFF
650            } else {
651                0xAAAAAA
652            };
653            draw_string_into_buffer(
654                buf,
655                buf_width,
656                panel.label().as_bytes(),
657                10,
658                item_y + 4,
659                color,
660            );
661        }
662
663        // -- sidebar / content divider --
664        for y in 0..buf_height {
665            let off = (y * buf_width + SIDEBAR_WIDTH) * 4;
666            if off + 3 < buf.len() {
667                buf[off] = 0x55;
668                buf[off + 1] = 0x55;
669                buf[off + 2] = 0x55;
670                buf[off + 3] = 0xFF;
671            }
672        }
673
674        // -- content area --
675        match self.active_panel {
676            SettingsPanel::Display => {
677                self.render_display_panel(buf, buf_width);
678            }
679            SettingsPanel::Network => {
680                self.render_network_panel(buf, buf_width);
681            }
682            SettingsPanel::Users => {
683                self.render_users_panel(buf, buf_width);
684            }
685            SettingsPanel::Appearance => {
686                self.render_appearance_panel(buf, buf_width);
687            }
688            SettingsPanel::Audio => {
689                self.render_audio_panel(buf, buf_width);
690            }
691            SettingsPanel::Bluetooth => {
692                self.render_bluetooth_panel(buf, buf_width);
693            }
694            SettingsPanel::Power => {
695                self.render_power_panel(buf, buf_width);
696            }
697            SettingsPanel::About => {
698                self.render_about_panel(buf, buf_width);
699            }
700        }
701    }
702
703    /// Render the settings window into a `u32` BGRA pixel buffer.
704    ///
705    /// `buffer` must be at least `buf_width * buf_height` elements.
706    pub fn render_to_buffer(&self, buffer: &mut [u32], buf_width: usize, buf_height: usize) {
707        let byte_len = buf_width * buf_height * 4;
708        let mut byte_buf = vec![0u8; byte_len];
709        self.render_to_u8_buffer(&mut byte_buf, buf_width, buf_height);
710
711        // Convert byte buffer (BGRA u8) into u32 buffer
712        for (i, chunk) in byte_buf.chunks_exact(4).enumerate() {
713            if i < buffer.len() {
714                buffer[i] = (chunk[3] as u32) << 24
715                    | (chunk[2] as u32) << 16
716                    | (chunk[1] as u32) << 8
717                    | (chunk[0] as u32);
718            }
719        }
720    }
721
722    // -- per-panel rendering helpers ----------------------------------------
723
724    fn render_label_value(
725        buf: &mut [u8],
726        buf_width: usize,
727        row: usize,
728        label: &[u8],
729        value: &[u8],
730        selected: bool,
731    ) {
732        let y = CONTENT_Y + row * LINE_HEIGHT;
733
734        // Selection highlight
735        if selected {
736            for dy in 0..LINE_HEIGHT {
737                for x in CONTENT_X..buf_width {
738                    let off = ((y + dy) * buf_width + x) * 4;
739                    if off + 3 < buf.len() {
740                        buf[off] = 0x44;
741                        buf[off + 1] = 0x3A;
742                        buf[off + 2] = 0x30;
743                        buf[off + 3] = 0xFF;
744                    }
745                }
746            }
747        }
748
749        let label_color: u32 = 0x999999;
750        let value_color: u32 = if selected { 0xFFCC66 } else { 0xDDDDDD };
751
752        draw_string_into_buffer(buf, buf_width, label, CONTENT_X, y + 3, label_color);
753        let val_x = CONTENT_X + (label.len() + 1) * CHAR_W;
754        draw_string_into_buffer(buf, buf_width, value, val_x, y + 3, value_color);
755    }
756
757    fn render_display_panel(&self, buf: &mut [u8], buf_width: usize) {
758        let res = if self.display.resolution_index < self.display.available_resolutions.len() {
759            let (w, h) = self.display.available_resolutions[self.display.resolution_index];
760            format!("{}x{}", w, h)
761        } else {
762            String::from("unknown")
763        };
764
765        Self::render_label_value(
766            buf,
767            buf_width,
768            0,
769            b"Resolution:",
770            res.as_bytes(),
771            self.selected_item == 0,
772        );
773
774        let bright = format!("{}%", self.display.brightness);
775        Self::render_label_value(
776            buf,
777            buf_width,
778            1,
779            b"Brightness:",
780            bright.as_bytes(),
781            self.selected_item == 1,
782        );
783    }
784
785    fn render_network_panel(&self, buf: &mut [u8], buf_width: usize) {
786        Self::render_label_value(
787            buf,
788            buf_width,
789            0,
790            b"Hostname:",
791            self.network.hostname.as_bytes(),
792            self.selected_item == 0,
793        );
794
795        let dhcp_str = if self.network.dhcp_enabled {
796            "ON"
797        } else {
798            "OFF"
799        };
800        Self::render_label_value(
801            buf,
802            buf_width,
803            1,
804            b"DHCP:",
805            dhcp_str.as_bytes(),
806            self.selected_item == 1,
807        );
808
809        Self::render_label_value(
810            buf,
811            buf_width,
812            2,
813            b"IP Address:",
814            self.network.ip_address.as_bytes(),
815            self.selected_item == 2,
816        );
817
818        Self::render_label_value(
819            buf,
820            buf_width,
821            3,
822            b"Gateway:",
823            self.network.gateway.as_bytes(),
824            self.selected_item == 3,
825        );
826
827        Self::render_label_value(
828            buf,
829            buf_width,
830            4,
831            b"DNS:",
832            self.network.dns.as_bytes(),
833            self.selected_item == 4,
834        );
835    }
836
837    fn render_users_panel(&self, buf: &mut [u8], buf_width: usize) {
838        Self::render_label_value(
839            buf,
840            buf_width,
841            0,
842            b"Username:",
843            self.user.username.as_bytes(),
844            self.selected_item == 0,
845        );
846
847        Self::render_label_value(
848            buf,
849            buf_width,
850            1,
851            b"Shell:",
852            self.user.shell.as_bytes(),
853            self.selected_item == 1,
854        );
855
856        Self::render_label_value(
857            buf,
858            buf_width,
859            2,
860            b"Home:",
861            self.user.home_dir.as_bytes(),
862            self.selected_item == 2,
863        );
864    }
865
866    fn render_appearance_panel(&self, buf: &mut [u8], buf_width: usize) {
867        let theme = match self.appearance.theme_index {
868            0 => "Dark",
869            1 => "Light",
870            2 => "Solarized Dark",
871            3 => "Solarized Light",
872            4 => "Nord",
873            5 => "Dracula",
874            _ => "Custom",
875        };
876        Self::render_label_value(
877            buf,
878            buf_width,
879            0,
880            b"Theme:",
881            theme.as_bytes(),
882            self.selected_item == 0,
883        );
884
885        let fsz = format!("{}px", self.appearance.font_size);
886        Self::render_label_value(
887            buf,
888            buf_width,
889            1,
890            b"Font Size:",
891            fsz.as_bytes(),
892            self.selected_item == 1,
893        );
894
895        let icons = if self.appearance.show_desktop_icons {
896            "ON"
897        } else {
898            "OFF"
899        };
900        Self::render_label_value(
901            buf,
902            buf_width,
903            2,
904            b"Desktop Icons:",
905            icons.as_bytes(),
906            self.selected_item == 2,
907        );
908
909        let pos = match self.appearance.panel_position {
910            PanelPosition::Top => "Top",
911            PanelPosition::Bottom => "Bottom",
912        };
913        Self::render_label_value(
914            buf,
915            buf_width,
916            3,
917            b"Panel Position:",
918            pos.as_bytes(),
919            self.selected_item == 3,
920        );
921    }
922
923    fn render_about_panel(&self, buf: &mut [u8], buf_width: usize) {
924        // Wire real memory data
925        let mem = crate::mm::get_memory_stats();
926        let total_mb = (mem.total_frames * 4096) / (1024 * 1024);
927        let used_frames = mem.total_frames.saturating_sub(mem.free_frames);
928        let used_mb = (used_frames * 4096) / (1024 * 1024);
929        let mem_str = format!("{}/{} MB", used_mb, total_mb);
930
931        // CPU architecture
932        let arch_str = if cfg!(target_arch = "x86_64") {
933            "x86_64"
934        } else if cfg!(target_arch = "aarch64") {
935            "aarch64"
936        } else if cfg!(target_arch = "riscv64") {
937            "riscv64gc"
938        } else {
939            "unknown"
940        };
941
942        let rows: [(&[u8], &[u8]); 7] = [
943            (b"OS:", self.about.os_name.as_bytes()),
944            (b"Version:", self.about.version.as_bytes()),
945            (b"Kernel:", self.about.kernel_version.as_bytes()),
946            (b"Package:", self.about.arch.as_bytes()),
947            (b"Hostname:", self.about.hostname.as_bytes()),
948            (b"Arch:", arch_str.as_bytes()),
949            (b"Memory:", mem_str.as_bytes()),
950        ];
951
952        for (i, (label, value)) in rows.iter().enumerate() {
953            // About panel is read-only; never highlight.
954            Self::render_label_value(buf, buf_width, i, label, value, false);
955        }
956    }
957
958    fn render_audio_panel(&self, buf: &mut [u8], buf_width: usize) {
959        let vol = format!(
960            "{}%{}",
961            self.audio.master_volume,
962            if self.audio.muted { " (muted)" } else { "" }
963        );
964        Self::render_label_value(
965            buf,
966            buf_width,
967            0,
968            b"Volume:",
969            vol.as_bytes(),
970            self.selected_item == 0,
971        );
972
973        let mute = if self.audio.muted { "Yes" } else { "No" };
974        Self::render_label_value(
975            buf,
976            buf_width,
977            1,
978            b"Muted:",
979            mute.as_bytes(),
980            self.selected_item == 1,
981        );
982
983        Self::render_label_value(
984            buf,
985            buf_width,
986            2,
987            b"Output:",
988            self.audio.output_device.as_bytes(),
989            self.selected_item == 2,
990        );
991    }
992
993    fn render_bluetooth_panel(&self, buf: &mut [u8], buf_width: usize) {
994        let enabled = if self.bluetooth.enabled { "On" } else { "Off" };
995        Self::render_label_value(
996            buf,
997            buf_width,
998            0,
999            b"Bluetooth:",
1000            enabled.as_bytes(),
1001            self.selected_item == 0,
1002        );
1003
1004        let disc = if self.bluetooth.discoverable {
1005            "Yes"
1006        } else {
1007            "No"
1008        };
1009        Self::render_label_value(
1010            buf,
1011            buf_width,
1012            1,
1013            b"Discoverable:",
1014            disc.as_bytes(),
1015            self.selected_item == 1,
1016        );
1017
1018        Self::render_label_value(
1019            buf,
1020            buf_width,
1021            2,
1022            b"Device Name:",
1023            self.bluetooth.device_name.as_bytes(),
1024            self.selected_item == 2,
1025        );
1026
1027        // Paired devices section
1028        let y = CONTENT_Y + 4 * LINE_HEIGHT;
1029        draw_string_into_buffer(
1030            buf,
1031            buf_width,
1032            b"Paired Devices: (none)",
1033            CONTENT_X,
1034            y,
1035            0x777777,
1036        );
1037    }
1038
1039    fn render_power_panel(&self, buf: &mut [u8], buf_width: usize) {
1040        let profile = if self.power_settings.profile_index < self.power_settings.profiles.len() {
1041            self.power_settings.profiles[self.power_settings.profile_index]
1042        } else {
1043            "Unknown"
1044        };
1045        Self::render_label_value(
1046            buf,
1047            buf_width,
1048            0,
1049            b"Power Profile:",
1050            profile.as_bytes(),
1051            self.selected_item == 0,
1052        );
1053
1054        let timeout = if self.power_settings.screen_timeout_min == 0 {
1055            String::from("Never")
1056        } else {
1057            format!("{} min", self.power_settings.screen_timeout_min)
1058        };
1059        Self::render_label_value(
1060            buf,
1061            buf_width,
1062            1,
1063            b"Screen Timeout:",
1064            timeout.as_bytes(),
1065            self.selected_item == 1,
1066        );
1067
1068        // Power status from kernel
1069        let y = CONTENT_Y + 3 * LINE_HEIGHT;
1070        let governor = match crate::power::get_governor() {
1071            crate::power::Governor::OnDemand => "OnDemand",
1072            crate::power::Governor::Performance => "Performance",
1073            crate::power::Governor::PowerSave => "PowerSave",
1074        };
1075        let gov_label = format!("Governor: {}", governor);
1076        draw_string_into_buffer(buf, buf_width, gov_label.as_bytes(), CONTENT_X, y, 0x777777);
1077    }
1078}
1079
1080impl Default for SettingsApp {
1081    fn default() -> Self {
1082        Self::new()
1083    }
1084}