ferrix_app/
messages.rs

1/* messages.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
21//! UI events handler & Data Updater
22
23use ferrix_lib::{
24    battery::BatInfo,
25    cpu::{Processors, Stat},
26    cpu_freq::CpuFreq,
27    drm::Video,
28    init::{Connection, SystemdServices},
29    parts::Mounts,
30    ram::{RAM, Swaps},
31    soft::InstalledPackages,
32    sys::{Groups, KModules, Kernel, OsRelease, Users},
33    traits::ToJson,
34    vulnerabilities::Vulnerabilities,
35};
36use iced::{Task, color, time::Instant};
37
38use crate::{
39    DataLoadingState, Ferrix, Page, SETTINGS_PATH, System,
40    dmi::DMIData,
41    export::{ExportData, ExportFormat, ExportMode},
42    settings::Style,
43    styles::CPU_CHARTS_COLORS,
44    utils::get_home,
45    widgets::line_charts::LineSeries,
46};
47
48#[derive(Debug, Clone)]
49pub enum Message {
50    DataReceiver(DataReceiverMessage),
51    ExportManager(ExportManagerMessage),
52    Settings(SettingsMessage),
53    Buttons(ButtonsMessage),
54
55    SelectPage(Page),
56    Dummy,
57}
58
59impl Message {
60    pub fn update<'a>(self, state: &'a mut Ferrix) -> Task<Message> {
61        match self {
62            Self::DataReceiver(data) => data.update(state),
63            Self::ExportManager(export) => export.update(state),
64            Self::Settings(settings) => settings.update(state),
65            Self::Buttons(buttons) => buttons.update(state),
66
67            Self::SelectPage(page) => state.select_page(page),
68            Self::Dummy => Task::none(),
69        }
70    }
71}
72
73impl Ferrix {
74    fn select_page(&mut self, page: Page) -> Task<Message> {
75        self.current_page = page;
76        Task::none()
77    }
78}
79
80#[derive(Debug, Clone)]
81pub enum DataReceiverMessage {
82    GetCPUData,
83    CPUDataReceived(DataLoadingState<Processors>),
84
85    AnimationTick(Instant),
86    ToggleStacked,
87
88    // AddTotalCPUUsage,
89    AddCPUCoreLineSeries,
90    ChangeShowCPUChartElements(usize),
91
92    GetProcStat,
93    ProcStatReceived(DataLoadingState<Stat>),
94
95    GetCPUFrequency,
96    CPUFrequencyReceived(DataLoadingState<CpuFreq>),
97
98    GetCPUVulnerabilities,
99    CPUVulnerabilitiesReveived(DataLoadingState<Vulnerabilities>),
100
101    GetRAMData,
102    RAMDataReceived(DataLoadingState<RAM>),
103
104    GetSwapData,
105    SwapDataReceived(DataLoadingState<Swaps>),
106
107    AddTotalRAMUsage,
108
109    GetStorageData,
110    StorageDataReceived(DataLoadingState<Mounts>),
111
112    GetDMIData,
113    DMIDataReceived(DataLoadingState<DMIData>),
114
115    GetBatInfo,
116    BatInfoReceived(DataLoadingState<BatInfo>),
117
118    GetDRMData,
119    DRMDataReceived(DataLoadingState<Video>),
120
121    GetOsReleaseData,
122    OsReleaseDataReceived(DataLoadingState<OsRelease>),
123
124    GetKernelData,
125    KernelDataReceived(DataLoadingState<Kernel>),
126
127    GetKModsData,
128    KModsDataReceived(DataLoadingState<KModules>),
129
130    GetUsersData,
131    UsersDataReceived(DataLoadingState<Users>),
132
133    GetGroupsData,
134    GroupsDataReceived(DataLoadingState<Groups>),
135
136    GetSystemdServices,
137    SystemdServicesReceived(DataLoadingState<SystemdServices>),
138
139    GetPackagesList,
140    PackagesListReceived(DataLoadingState<InstalledPackages>),
141
142    GetSystemData,
143    SystemDataReceived(DataLoadingState<System>),
144}
145
146impl DataReceiverMessage {
147    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
148        match self {
149            Self::CPUDataReceived(state) => {
150                fx.proc_data = state;
151                Task::none()
152            }
153            Self::GetCPUData => Task::perform(
154                async move {
155                    let proc = Processors::new();
156                    match proc {
157                        Ok(proc) => DataLoadingState::Loaded(proc),
158                        Err(why) => DataLoadingState::Error(why.to_string()),
159                    }
160                },
161                |val| Message::DataReceiver(Self::CPUDataReceived(val)),
162            ),
163            Self::AnimationTick(now) => {
164                if let Page::SystemMonitor = fx.current_page {
165                    fx.cpu_usage_chart.set_max_y(100.);
166                    fx.ram_usage_chart.set_max_y(100.);
167                }
168                fx.cpu_usage_chart.tick(now);
169                fx.ram_usage_chart.tick(now);
170
171                Task::none()
172            }
173            Self::ToggleStacked => {
174                fx.cpu_usage_chart.toggle_stacked();
175                fx.ram_usage_chart.toggle_stacked();
176
177                Task::none()
178            }
179            Self::ProcStatReceived(state) => {
180                if fx.curr_proc_stat.is_some() {
181                    fx.prev_proc_stat = fx.curr_proc_stat.clone();
182                } else if fx.curr_proc_stat.is_none() && fx.prev_proc_stat.is_none() {
183                    fx.prev_proc_stat = state.clone();
184                }
185                fx.curr_proc_stat = state;
186                Task::none()
187            }
188            Self::GetProcStat => Task::perform(
189                async move {
190                    let stat = Stat::new();
191                    match stat {
192                        Ok(stat) => DataLoadingState::Loaded(stat),
193                        Err(why) => DataLoadingState::Error(why.to_string()),
194                    }
195                },
196                |val| Message::DataReceiver(Self::ProcStatReceived(val)),
197            ),
198            // Self::AddTotalCPUUsage => {
199            //     fx.cpu_usage_chart.push_value(match &fx.curr_proc_stat {
200            //         DataLoadingState::Loaded(val) => val.cpu.unwrap().usage_percentage({
201            //             let prev = fx.prev_proc_stat.clone().unwrap();
202            //             prev.cpu
203            //         }) as f64,
204            //         _ => 0.,
205            //     });
206            //     Task::none()
207            // }
208            Self::AddCPUCoreLineSeries => {
209                let curr_proc = &fx.curr_proc_stat;
210                let prev_proc = &fx.prev_proc_stat;
211
212                if curr_proc.is_none() || prev_proc.is_none() {
213                    return Task::none();
214                }
215                let curr_proc = curr_proc.to_option().unwrap();
216                let prev_proc = prev_proc.to_option().unwrap();
217
218                if curr_proc.cpus.len() != prev_proc.cpus.len() {
219                    return Task::none();
220                }
221                let len = curr_proc.cpus.len();
222
223                let colors = CPU_CHARTS_COLORS;
224                for id in 0..len {
225                    if fx.show_cpus_chart.get(&id).is_none() {
226                        let color = {
227                            if colors.len() - 1 < id {
228                                color!(255, 255, 255)
229                            } else {
230                                colors[id]
231                            }
232                        };
233                        let mut line =
234                            LineSeries::new(format!("CPU #{id}"), color, fx.show_chart_elements);
235                        line.push(
236                            curr_proc.cpus[id].usage_percentage(Some(prev_proc.cpus[id])) as f64,
237                        );
238                        fx.cpu_usage_chart.push_series(line);
239                        fx.show_cpus_chart.insert(id);
240                    } else {
241                        fx.cpu_usage_chart.push_to(
242                            id,
243                            "",
244                            curr_proc.cpus[id].usage_percentage(Some(prev_proc.cpus[id])) as f64,
245                        );
246                    }
247                }
248
249                Task::none()
250            }
251            Self::ChangeShowCPUChartElements(elems) => {
252                fx.show_chart_elements = elems;
253
254                fx.cpu_usage_chart.set_max_values(elems);
255                fx.ram_usage_chart.set_max_values(elems);
256
257                Task::none()
258            }
259            Self::CPUFrequencyReceived(state) => {
260                fx.cpu_freq = state;
261                Task::none()
262            }
263            Self::GetCPUFrequency => Task::perform(
264                async move {
265                    let cpu_freq = CpuFreq::new();
266                    match cpu_freq {
267                        Ok(cpu_freq) => DataLoadingState::Loaded(cpu_freq),
268                        Err(why) => DataLoadingState::Error(why.to_string()),
269                    }
270                },
271                |val| Message::DataReceiver(DataReceiverMessage::CPUFrequencyReceived(val)),
272            ),
273            Self::CPUVulnerabilitiesReveived(state) => {
274                fx.cpu_vulnerabilities = state;
275                Task::none()
276            }
277            Self::GetCPUVulnerabilities => Task::perform(
278                async move {
279                    let vuln = Vulnerabilities::new();
280                    match vuln {
281                        Ok(vuln) => DataLoadingState::Loaded(vuln),
282                        Err(why) => DataLoadingState::Error(why.to_string()),
283                    }
284                },
285                |val| Message::DataReceiver(Self::CPUVulnerabilitiesReveived(val)),
286            ),
287            Self::StorageDataReceived(state) => {
288                fx.storages = state;
289                Task::none()
290            }
291            Self::GetStorageData => Task::perform(
292                async move {
293                    let storage = Mounts::new();
294                    match storage {
295                        Ok(storage) => DataLoadingState::Loaded(storage),
296                        Err(why) => DataLoadingState::Error(why.to_string()),
297                    }
298                },
299                |val| Message::DataReceiver(DataReceiverMessage::StorageDataReceived(val)),
300            ),
301            Self::DMIDataReceived(state) => {
302                if state.some_value() && fx.is_polkit {
303                    fx.dmi_data = state;
304                } else if !fx.is_polkit {
305                    fx.dmi_data = state;
306                }
307                Task::none()
308            }
309            Self::GetDMIData => {
310                if !fx.is_polkit && fx.dmi_data.is_none() && fx.current_page == Page::DMI {
311                    fx.is_polkit = true;
312                    Task::perform(async move { crate::dmi::get_dmi_data().await }, |val| {
313                        Message::DataReceiver(Self::DMIDataReceived(val))
314                    })
315                } else {
316                    Task::none()
317                }
318            }
319            Self::BatInfoReceived(state) => {
320                fx.bat_data = state;
321                Task::none()
322            }
323            Self::GetBatInfo => Task::perform(
324                async move {
325                    let bat = BatInfo::new();
326                    match bat {
327                        Ok(bat) => DataLoadingState::Loaded(bat),
328                        Err(why) => DataLoadingState::Error(why.to_string()),
329                    }
330                },
331                |val| Message::DataReceiver(Self::BatInfoReceived(val)),
332            ),
333            Self::DRMDataReceived(state) => {
334                fx.drm_data = state;
335                Task::none()
336            }
337            Self::GetDRMData => Task::perform(
338                async move {
339                    let drm = Video::new();
340                    match drm {
341                        Ok(drm) => DataLoadingState::Loaded(drm),
342                        Err(why) => DataLoadingState::Error(why.to_string()),
343                    }
344                },
345                |val| Message::DataReceiver(Self::DRMDataReceived(val)),
346            ),
347            Self::RAMDataReceived(state) => {
348                fx.ram_data = state;
349                Task::none()
350            }
351            Self::GetRAMData => Task::perform(
352                async move {
353                    let ram = RAM::new();
354                    match ram {
355                        Ok(ram) => DataLoadingState::Loaded(ram),
356                        Err(why) => DataLoadingState::Error(why.to_string()),
357                    }
358                },
359                |val| Message::DataReceiver(Self::RAMDataReceived(val)),
360            ),
361            Self::SwapDataReceived(state) => {
362                fx.swap_data = state;
363                Task::none()
364            }
365            Self::GetSwapData => Task::perform(
366                async move {
367                    let swap = Swaps::new();
368                    match swap {
369                        Ok(swap) => DataLoadingState::Loaded(swap),
370                        Err(why) => DataLoadingState::Error(why.to_string()),
371                    }
372                },
373                |val| Message::DataReceiver(Self::SwapDataReceived(val)),
374            ),
375            Self::AddTotalRAMUsage => {
376                let ram = &fx.ram_data;
377                let swap = &fx.swap_data;
378
379                if ram.is_none() {
380                    return Task::none();
381                }
382                let ram = ram.to_option().unwrap();
383                let ram_color = color!(128, 64, 255);
384
385                let ram_usage = ram.usage_percentage().unwrap_or(0.);
386
387                if fx.ram_usage_chart.series_count() == 0 {
388                    let mut ram_line =
389                        LineSeries::new(format!("RAM"), ram_color, fx.show_chart_elements);
390                    ram_line.push(ram_usage);
391                    fx.ram_usage_chart.push_series(ram_line);
392                } else {
393                    fx.ram_usage_chart.push_to(0, "", ram_usage);
394                }
395
396                if let Some(swap) = swap.to_option() {
397                    let len = swap.swaps.len();
398                    let colors = CPU_CHARTS_COLORS;
399
400                    let current_series_cnt = fx.ram_usage_chart.series_count();
401
402                    for id in 0..len {
403                        let series_idx = id + 1;
404                        let swap_usage = swap.swaps[id].usage_percentage().unwrap_or(0.);
405
406                        if series_idx >= current_series_cnt {
407                            let color = if colors.len() - 1 < id {
408                                color!(255, 255, 128)
409                            } else {
410                                colors[id]
411                            };
412                            let mut line = LineSeries::new(
413                                &swap.swaps[id].filename,
414                                color,
415                                fx.show_chart_elements,
416                            );
417                            line.push(swap_usage);
418
419                            fx.ram_usage_chart.push_series(line);
420                        } else {
421                            fx.ram_usage_chart.push_to(series_idx, "", swap_usage);
422                        }
423                    }
424                }
425                Task::none()
426            }
427            Self::OsReleaseDataReceived(state) => {
428                fx.osrel_data = state;
429                Task::none()
430            }
431            Self::GetOsReleaseData => Task::perform(
432                async move {
433                    let osrel = OsRelease::new();
434                    match osrel {
435                        Ok(osrel) => DataLoadingState::Loaded(osrel),
436                        Err(why) => DataLoadingState::Error(why.to_string()),
437                    }
438                },
439                |val| Message::DataReceiver(Self::OsReleaseDataReceived(val)),
440            ),
441            Self::KernelDataReceived(state) => {
442                fx.kernel_data = state;
443                Task::none()
444            }
445            Self::GetKernelData => Task::perform(
446                async move {
447                    let kern = Kernel::new();
448                    match kern {
449                        Ok(kern) => {
450                            // kern.mods.modules.sort_by_key(|md| md.name.clone());
451                            DataLoadingState::Loaded(kern)
452                        }
453                        Err(why) => DataLoadingState::Error(why.to_string()),
454                    }
455                },
456                |val| Message::DataReceiver(Self::KernelDataReceived(val)),
457            ),
458            Self::KModsDataReceived(state) => {
459                fx.kmods_data = state;
460                Task::none()
461            }
462            Self::GetKModsData => Task::perform(
463                async move {
464                    let kern = KModules::new();
465                    match kern {
466                        Ok(mut kern) => {
467                            kern.modules.sort_by_key(|module| module.name.clone());
468                            DataLoadingState::Loaded(kern)
469                        }
470                        Err(why) => DataLoadingState::Error(why.to_string()),
471                    }
472                },
473                |val| Message::DataReceiver(Self::KModsDataReceived(val)),
474            ),
475            Self::UsersDataReceived(state) => {
476                fx.users_list = state;
477                Task::none()
478            }
479            Self::GetUsersData => Task::perform(
480                async move {
481                    let users = Users::new();
482                    match users {
483                        Ok(mut users) => {
484                            users.users.sort_by_key(|usr| usr.uid);
485                            DataLoadingState::Loaded(users)
486                        }
487                        Err(why) => DataLoadingState::Error(why.to_string()),
488                    }
489                },
490                |val| Message::DataReceiver(Self::UsersDataReceived(val)),
491            ),
492            Self::GroupsDataReceived(state) => {
493                fx.groups_list = state;
494                Task::none()
495            }
496            Self::GetGroupsData => Task::perform(
497                async move {
498                    let groups = Groups::new();
499                    match groups {
500                        Ok(mut groups) => {
501                            groups.groups.sort_by_key(|grp| grp.gid);
502                            DataLoadingState::Loaded(groups)
503                        }
504                        Err(why) => DataLoadingState::Error(why.to_string()),
505                    }
506                },
507                |val| Message::DataReceiver(Self::GroupsDataReceived(val)),
508            ),
509            Self::SystemdServicesReceived(state) => {
510                fx.sysd_services_list = state;
511                Task::none()
512            }
513            Self::GetSystemdServices => Task::perform(
514                async move {
515                    let conn = Connection::session().await;
516                    if let Err(why) = conn {
517                        return DataLoadingState::Error(why.to_string());
518                    }
519                    let conn = conn.unwrap();
520
521                    let srv_list = SystemdServices::new_from_connection(&conn).await;
522                    match srv_list {
523                        Ok(srv_list) => DataLoadingState::Loaded(srv_list),
524                        Err(why) => DataLoadingState::Error(why.to_string()),
525                    }
526                },
527                |val| Message::DataReceiver(Self::SystemdServicesReceived(val)),
528            ),
529            Self::SystemDataReceived(state) => {
530                fx.system = state;
531                Task::none()
532            }
533            Self::GetPackagesList => Task::perform(
534                async move {
535                    let pkglist = InstalledPackages::get();
536                    match pkglist {
537                        Ok(pkglist) => DataLoadingState::Loaded(pkglist),
538                        Err(why) => DataLoadingState::Error(why.to_string()),
539                    }
540                },
541                |val| Message::DataReceiver(Self::PackagesListReceived(val)),
542            ),
543            Self::PackagesListReceived(state) => {
544                fx.installed_pkgs_list = state;
545                Task::none()
546            }
547            Self::GetSystemData => Task::perform(
548                async move {
549                    let sys = System::new();
550                    match sys {
551                        Ok(sys) => DataLoadingState::Loaded(sys),
552                        Err(why) => DataLoadingState::Error(why.to_string()),
553                    }
554                },
555                |val| Message::DataReceiver(Self::SystemDataReceived(val)),
556            ),
557        }
558    }
559}
560
561pub type ExportToFilePath = String;
562
563#[derive(Debug, Clone)]
564pub enum ExportManagerMessage {
565    ExportData(ExportToFilePath),
566    ExportFormatSelected(ExportFormat),
567    ExportModeSelected(ExportMode),
568}
569
570impl ExportManagerMessage {
571    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
572        match self {
573            Self::ExportData(path) => fx.export_data(&path),
574            _ => Task::none(),
575        }
576    }
577}
578
579impl Ferrix {
580    fn export_data(&mut self, path: &str) -> Task<Message> {
581        let json = ExportData::from(self)
582            .to_json()
583            .unwrap_or("{error}".to_string());
584        let _ = std::fs::write(path, json);
585        Task::none()
586    }
587}
588
589#[derive(Debug, Clone)]
590pub enum SettingsMessage {
591    ChangeStyle(Style),
592    ChangeUpdatePeriod(u8),
593}
594
595impl SettingsMessage {
596    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
597        match self {
598            Self::ChangeStyle(style) => fx.change_style(style),
599            Self::ChangeUpdatePeriod(secs) => fx.change_update_period(secs),
600        }
601    }
602}
603
604impl Ferrix {
605    fn change_style(&mut self, style: Style) -> Task<Message> {
606        self.settings.style = style;
607        Task::none()
608    }
609
610    fn change_update_period(&mut self, per: u8) -> Task<Message> {
611        self.settings.update_period = per;
612        Task::none()
613    }
614}
615
616#[derive(Debug, Clone)]
617pub enum ButtonsMessage {
618    LinkButtonPressed(String),
619    SaveSettingsButtonPressed,
620    CopyButtonPressed(String),
621    ShowToastToggle,
622}
623
624impl ButtonsMessage {
625    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
626        match self {
627            Self::LinkButtonPressed(url) => fx.go_to_url(&url),
628            Self::SaveSettingsButtonPressed => fx.save_settings(),
629            Self::CopyButtonPressed(s) => {
630                fx.show_copy_toast = true;
631                iced::clipboard::write(s)
632            },
633            Self::ShowToastToggle => self.show_toast_toggle(fx),
634        }
635    }
636
637    fn show_toast_toggle<'a>(&self, fx: &'a mut Ferrix) -> Task<Message> {
638        fx.show_copy_toast = !fx.show_copy_toast;
639        Task::none()
640    }
641}
642
643impl Ferrix {
644    fn go_to_url(&self, url: &str) -> Task<Message> {
645        // TODO: add error handling
646        let _ = crate::utils::xdg_open(url);
647        Task::none()
648    }
649
650    fn save_settings(&mut self) -> Task<Message> {
651        // TODO: add error handling
652        let _ = self
653            .settings
654            .write(get_home().join(".config").join(SETTINGS_PATH));
655        Task::none()
656    }
657}