ferrix_app/
lib.rs

1/* lib.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21pub mod export;
22pub mod i18n;
23pub mod icons;
24pub mod load_state;
25pub mod modals;
26pub mod pages;
27pub mod styles;
28pub mod utils;
29pub mod widgets;
30
31pub mod dmi;
32pub mod kernel;
33
34// REFACTORED MODULES
35pub mod messages;
36// pub mod ferrix; // TODO!
37use messages::*;
38
39pub use load_state::DataLoadingState;
40pub use pages::*;
41
42use dmi::DMIResult;
43use serde::{Deserialize, Serialize};
44use widgets::{icon_button, sidebar_button};
45
46use anyhow::Result;
47use ferrix_lib::{
48    battery::BatInfo,
49    cpu::{Processors, Stat},
50    drm::Video,
51    init::SystemdServices,
52    ram::RAM,
53    sys::{
54        Groups, LoadAVG, OsRelease, Uptime, Users, get_current_desktop, get_env_vars, get_hostname,
55        get_lang,
56    },
57};
58use iced::{
59    time, widget::{column, container, row, scrollable, text}, Alignment::Center, Element, Length, Padding, Subscription, Task, Theme
60};
61use std::{fmt::Display, fs, path::Path, time::Duration};
62
63use crate::utils::get_home;
64
65const SETTINGS_PATH: &str = "./ferrix.conf";
66
67#[derive(Debug)]
68pub struct Ferrix {
69    pub current_page: Page,
70    pub proc_data: DataLoadingState<Processors>,
71    pub prev_proc_stat: DataLoadingState<Stat>,
72    pub curr_proc_stat: DataLoadingState<Stat>,
73    pub ram_data: DataLoadingState<RAM>,
74    pub dmi_data: DataLoadingState<DMIResult>,
75    pub bat_data: DataLoadingState<BatInfo>,
76    pub drm_data: DataLoadingState<Video>,
77    pub osrel_data: DataLoadingState<OsRelease>,
78    pub info_kernel: DataLoadingState<KernelData>,
79    pub users_list: DataLoadingState<Users>,
80    pub groups_list: DataLoadingState<Groups>,
81    pub sysd_services_list: DataLoadingState<SystemdServices>,
82    pub system: DataLoadingState<System>,
83    pub settings: FXSettings,
84    pub is_polkit: bool,
85}
86
87impl Default for Ferrix {
88    fn default() -> Self {
89        Self {
90            current_page: Page::default(),
91            proc_data: DataLoadingState::Loading,
92            prev_proc_stat: DataLoadingState::Loading,
93            curr_proc_stat: DataLoadingState::Loading,
94            ram_data: DataLoadingState::Loading,
95            dmi_data: DataLoadingState::Loading,
96            bat_data: DataLoadingState::Loading,
97            drm_data: DataLoadingState::Loading,
98            osrel_data: DataLoadingState::Loading,
99            info_kernel: DataLoadingState::Loading,
100            users_list: DataLoadingState::Loading,
101            groups_list: DataLoadingState::Loading,
102            sysd_services_list: DataLoadingState::Loading,
103            system: DataLoadingState::Loading,
104            settings: FXSettings::read(get_home().join(".config").join(SETTINGS_PATH))
105                .unwrap_or_default(),
106            is_polkit: false,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Serialize)]
112pub struct System {
113    pub hostname: Option<String>,
114    pub loadavg: Option<LoadAVG>,
115    pub uptime: Option<Uptime>,
116    pub desktop: Option<String>,
117    pub language: Option<String>,
118    pub env_vars: Vec<(String, String)>,
119}
120
121impl System {
122    pub fn new() -> Result<Self> {
123        Ok(Self {
124            hostname: get_hostname(),
125            loadavg: Some(LoadAVG::new()?),
126            uptime: Some(Uptime::new()?),
127            desktop: get_current_desktop(),
128            language: get_lang(),
129            env_vars: get_env_vars(),
130        })
131    }
132}
133
134#[derive(Debug, Clone, Deserialize, Serialize)]
135pub struct FXSettings {
136    pub update_period: u8,
137    pub style: Style,
138}
139
140impl FXSettings {
141    pub fn read<P: AsRef<Path>>(pth: P) -> Result<Self> {
142        let contents = fs::read_to_string(pth)?;
143        let data = toml::from_str(&contents)?;
144        Ok(data)
145    }
146
147    pub fn write<'a, P: AsRef<Path>>(&'a self, pth: P) -> Result<()> {
148        let contents = toml::to_string(&self)?;
149        fs::write(pth, contents)?;
150        Ok(())
151    }
152}
153
154impl Default for FXSettings {
155    fn default() -> Self {
156        Self {
157            update_period: 1,
158            style: Style::default(),
159        }
160    }
161}
162
163#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq)]
164pub enum Style {
165    Light,
166    #[default]
167    Dark,
168}
169
170impl Style {
171    pub const ALL: &[Self] = &[Self::Light, Self::Dark];
172
173    pub fn to_theme(&self) -> Theme {
174        match self {
175            Self::Light => Theme::GruvboxLight,
176            Self::Dark => Theme::GruvboxDark,
177        }
178    }
179}
180
181impl Display for Style {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        write!(
184            f,
185            "{}",
186            match self {
187                Self::Light => fl!("style-light"),
188                Self::Dark => fl!("style-dark"),
189            }
190        )
191    }
192}
193
194impl Ferrix {
195    pub fn theme(&self) -> Theme {
196        self.settings.style.to_theme()
197    }
198
199    pub fn update(&mut self, message: Message) -> Task<Message> {
200        message.update(self)
201    }
202
203    pub fn subscription(&self) -> Subscription<Message> {
204        let mut scripts = vec![
205            time::every(Duration::from_secs(self.settings.update_period as u64))
206                .map(|_| Message::DataReceiver(DataReceiverMessage::GetCPUData)),
207            time::every(Duration::from_secs(self.settings.update_period as u64))
208                .map(|_| Message::DataReceiver(DataReceiverMessage::GetProcStat)),
209        ];
210
211        if self.current_page == Page::Dashboard {
212            scripts.push(
213                time::every(Duration::from_secs(self.settings.update_period as u64))
214                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetRAMData)),
215            );
216        }
217
218        if self.osrel_data.is_none()
219            && (self.current_page == Page::Distro || self.current_page == Page::Dashboard)
220        {
221            scripts.push(
222                time::every(Duration::from_millis(10))
223                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetOsReleaseData)),
224            );
225        }
226
227        if self.drm_data.is_none() && self.current_page == Page::Screen {
228            scripts.push(
229                time::every(Duration::from_millis(10))
230                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetDRMData)),
231            );
232        } else if self.drm_data.is_some() && self.current_page == Page::Screen {
233            scripts.push(
234                time::every(Duration::from_secs(self.settings.update_period as u64))
235                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetDRMData)),
236            );
237        }
238
239        if self.bat_data.is_none() && self.current_page == Page::Battery {
240            scripts.push(
241                time::every(Duration::from_millis(10))
242                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetBatInfo)),
243            );
244        } else if self.bat_data.is_some() && self.current_page == Page::Battery {
245            scripts.push(
246                time::every(Duration::from_secs(self.settings.update_period as u64))
247                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetBatInfo)),
248            );
249        }
250
251        if self.info_kernel.is_none() && self.current_page == Page::Kernel {
252            scripts.push(
253                time::every(Duration::from_millis(10))
254                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetKernelData)),
255            );
256        }
257
258        if self.users_list.is_none() && self.current_page == Page::Users {
259            scripts.push(
260                time::every(Duration::from_millis(10))
261                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetUsersData)),
262            );
263        }
264
265        if self.system.is_none() {
266            scripts.push(
267                time::every(Duration::from_millis(10))
268                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetSystemData)),
269            );
270        }
271
272        if self.groups_list.is_none() && self.current_page == Page::Groups {
273            scripts.push(
274                time::every(Duration::from_millis(10))
275                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetGroupsData)),
276            );
277        }
278
279        if self.sysd_services_list.is_none() && self.current_page == Page::SystemManager {
280            scripts.push(
281                time::every(Duration::from_millis(10))
282                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetSystemdServices)),
283            );
284        } else if self.sysd_services_list.is_some() && self.current_page == Page::SystemManager {
285            scripts.push(
286                time::every(Duration::from_secs(
287                    self.settings.update_period as u64 * 10u64,
288                ))
289                .map(|_| Message::DataReceiver(DataReceiverMessage::GetSystemdServices)),
290            );
291        }
292
293        if self.system.is_none() && self.current_page == Page::SystemMisc {
294            scripts.push(
295                time::every(Duration::from_millis(10))
296                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetSystemData)),
297            );
298        } else if self.system.is_some() && self.current_page == Page::SystemMisc {
299            scripts.push(
300                time::every(Duration::from_secs(self.settings.update_period as u64))
301                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetSystemData)),
302            );
303        }
304
305        if self.current_page == Page::DMI && !self.is_polkit && self.dmi_data.is_none() {
306            scripts.push(
307                time::every(Duration::from_secs(1))
308                    .map(|_| Message::DataReceiver(DataReceiverMessage::GetDMIData)),
309            );
310        }
311
312        Subscription::batch(scripts)
313    }
314
315    pub fn view<'a>(&'a self) -> Element<'a, Message> {
316        row![sidebar(self.current_page), self.current_page.page(&self)]
317            .spacing(5)
318            .padding(5)
319            .into()
320    }
321}
322
323fn sidebar<'a>(cur_page: Page) -> container::Container<'a, Message> {
324    let buttons_bar = row![
325        icon_button("export", fl!("sidebar-export")).on_press(Message::SelectPage(Page::Export)),
326        icon_button("settings", fl!("sidebar-settings"))
327            .on_press(Message::SelectPage(Page::Settings)),
328        icon_button("about", fl!("sidebar-about")).on_press(Message::SelectPage(Page::About)),
329    ]
330    .spacing(2)
331    .align_y(Center);
332
333    let pages_bar = column![
334        text(fl!("sidebar-hardware")).style(text::secondary),
335        sidebar_button(Page::Dashboard, cur_page),
336        sidebar_button(Page::Processors, cur_page),
337        sidebar_button(Page::Memory, cur_page),
338        sidebar_button(Page::Storage, cur_page),
339        sidebar_button(Page::DMI, cur_page),
340        sidebar_button(Page::Battery, cur_page),
341        sidebar_button(Page::Screen, cur_page),
342        text(fl!("sidebar-admin")).style(text::secondary),
343        sidebar_button(Page::Distro, cur_page),
344        sidebar_button(Page::Users, cur_page),
345        sidebar_button(Page::Groups, cur_page),
346        sidebar_button(Page::SystemManager, cur_page),
347        sidebar_button(Page::Software, cur_page),
348        sidebar_button(Page::Environment, cur_page),
349        sidebar_button(Page::Sensors, cur_page),
350        text(fl!("sidebar-system")).style(text::secondary),
351        sidebar_button(Page::Kernel, cur_page),
352        sidebar_button(Page::KModules, cur_page),
353        sidebar_button(Page::Development, cur_page),
354        sidebar_button(Page::SystemMisc, cur_page),
355        text(fl!("sidebar-manage")).style(text::secondary),
356        sidebar_button(Page::Settings, cur_page),
357        sidebar_button(Page::About, cur_page),
358    ]
359    .padding(Padding::new(0.).right(5.))
360    .spacing(5);
361
362    container(column![buttons_bar, scrollable(pages_bar)].spacing(5))
363        .padding(5)
364        .style(container::bordered_box)
365        .height(Length::Fill)
366}