Skip to main content

ferrix_app/
messages.rs

1/* messages.rs
2 *
3 * Copyright 2025-2026 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::{BootTimestamps, Connection, SystemdServices},
29    net::Networks,
30    parts::Mounts,
31    ram::{RAM, Swaps},
32    soft::InstalledPackages,
33    sys::{Groups, KModules, Kernel, OsRelease, Users},
34    traits::ToJson,
35    vulnerabilities::Vulnerabilities,
36};
37use iced::{
38    Event, Task, color,
39    keyboard::{Event as Kevent, Key, Modifiers, key},
40    widget::{
41        Id,
42        operation::{self, AbsoluteOffset, RelativeOffset},
43    },
44};
45
46use crate::{
47    DataLoadingState, Page, SETTINGS_PATH, System,
48    dmi::DMIData,
49    export::{ExportData, ExportFormat, ExportMode},
50    ferrix::{Ferrix, FerrixData},
51    settings::{ChartLineThickness, FXSettings, Style},
52    styles::CPU_CHARTS_COLORS,
53    utils::{ToColor, get_home},
54    widgets::line_charts::LineSeries,
55};
56
57#[derive(Debug, Clone)]
58pub enum Message {
59    DataReceiver(DataReceiverMessage),
60    ExportManager(ExportManagerMessage),
61    Settings(SettingsMessage),
62    Buttons(ButtonsMessage),
63
64    SelectPage(Page),
65    Keyboard(KeyboardMessage),
66    Dummy,
67}
68
69impl Message {
70    pub fn update<'a>(self, state: &'a mut Ferrix) -> Task<Message> {
71        match self {
72            Self::DataReceiver(data) => {
73                data.update(&mut state.data, &mut state.settings, state.current_page)
74            }
75            Self::ExportManager(export) => export.update(state),
76            Self::Settings(settings) => settings.update(state),
77            Self::Buttons(buttons) => buttons.update(state),
78
79            Self::SelectPage(page) => state.select_page(page),
80            Self::Keyboard(keyboard) => keyboard.update(state),
81            Self::Dummy => Task::none(),
82        }
83    }
84}
85
86impl Ferrix {
87    fn select_page(&mut self, page: Page) -> Task<Message> {
88        self.current_page = page;
89        self.scrolled_area_id = page.scrolled_list_id();
90        Task::none()
91    }
92}
93
94#[derive(Debug, Clone)]
95pub enum DataReceiverMessage {
96    GetCPUData,
97    CPUDataReceived(DataLoadingState<Processors>),
98
99    // AddTotalCPUUsage,
100    AddCPUCoreLineSeries,
101    ChangeShowCPUChartElements(usize),
102
103    GetProcStat,
104    ProcStatReceived(DataLoadingState<Stat>),
105
106    GetCPUFrequency,
107    CPUFrequencyReceived(DataLoadingState<CpuFreq>),
108
109    GetCPUVulnerabilities,
110    CPUVulnerabilitiesReveived(DataLoadingState<Vulnerabilities>),
111
112    GetRAMData,
113    RAMDataReceived(DataLoadingState<RAM>),
114
115    GetSwapData,
116    SwapDataReceived(DataLoadingState<Swaps>),
117
118    AddTotalRAMUsage,
119
120    GetStorageData,
121    StorageDataReceived(DataLoadingState<Mounts>),
122
123    GetNetworksData,
124    NetworksDataReceived(DataLoadingState<Networks>),
125
126    GetDMIData,
127    DMIDataReceived(DataLoadingState<DMIData>),
128
129    GetBatInfo,
130    BatInfoReceived(DataLoadingState<BatInfo>),
131
132    GetDRMData,
133    DRMDataReceived(DataLoadingState<Video>),
134
135    GetOsReleaseData,
136    OsReleaseDataReceived(DataLoadingState<OsRelease>),
137
138    GetKernelData,
139    KernelDataReceived(DataLoadingState<Kernel>),
140
141    GetKModsData,
142    KModsDataReceived(DataLoadingState<KModules>),
143
144    GetUsersData,
145    UsersDataReceived(DataLoadingState<Users>),
146
147    GetGroupsData,
148    GroupsDataReceived(DataLoadingState<Groups>),
149
150    GetSystemdServices,
151    SystemdServicesReceived(
152        (
153            DataLoadingState<SystemdServices>,
154            DataLoadingState<BootTimestamps>,
155        ),
156    ),
157
158    GetPackagesList,
159    PackagesListReceived(DataLoadingState<InstalledPackages>),
160
161    GetSystemData,
162    SystemDataReceived(DataLoadingState<System>),
163}
164
165impl DataReceiverMessage {
166    pub fn update<'a>(
167        self,
168        fx: &'a mut FerrixData,
169        settings: &'a mut FXSettings,
170        cur_page: Page,
171    ) -> Task<Message> {
172        match self {
173            Self::CPUDataReceived(state) => {
174                fx.proc_data = state;
175                Task::none()
176            }
177            Self::GetCPUData => Task::perform(
178                async move {
179                    let proc = Processors::new();
180                    match proc {
181                        Ok(proc) => DataLoadingState::Loaded(proc),
182                        Err(why) => DataLoadingState::Error(why.to_string()),
183                    }
184                },
185                |val| Message::DataReceiver(Self::CPUDataReceived(val)),
186            ),
187            Self::ProcStatReceived(state) => {
188                if fx.curr_proc_stat.is_some() {
189                    fx.prev_proc_stat = fx.curr_proc_stat.clone();
190                } else if fx.curr_proc_stat.is_none() && fx.prev_proc_stat.is_none() {
191                    fx.prev_proc_stat = state.clone();
192                }
193                fx.curr_proc_stat = state;
194                Task::none()
195            }
196            Self::GetProcStat => Task::perform(
197                async move {
198                    let stat = Stat::new();
199                    match stat {
200                        Ok(stat) => DataLoadingState::Loaded(stat),
201                        Err(why) => DataLoadingState::Error(why.to_string()),
202                    }
203                },
204                |val| Message::DataReceiver(Self::ProcStatReceived(val)),
205            ),
206            // Self::AddTotalCPUUsage => {
207            //     fx.cpu_usage_chart.push_value(match &fx.curr_proc_stat {
208            //         DataLoadingState::Loaded(val) => val.cpu.unwrap().usage_percentage({
209            //             let prev = fx.prev_proc_stat.clone().unwrap();
210            //             prev.cpu
211            //         }) as f64,
212            //         _ => 0.,
213            //     });
214            //     Task::none()
215            // }
216            Self::AddCPUCoreLineSeries => {
217                let curr_proc = &fx.curr_proc_stat;
218                let prev_proc = &fx.prev_proc_stat;
219
220                if curr_proc.is_none() || prev_proc.is_none() {
221                    return Task::none();
222                }
223                let curr_proc = curr_proc.to_option().unwrap();
224                let prev_proc = prev_proc.to_option().unwrap();
225
226                if curr_proc.cpus.len() != prev_proc.cpus.len() {
227                    return Task::none();
228                }
229                let len = curr_proc.cpus.len();
230
231                let colors_set = &settings.chart_colors.colors;
232                let def_colors = CPU_CHARTS_COLORS;
233
234                for id in 0..len {
235                    let percent =
236                        curr_proc.cpus[id].usage_percentage(Some(prev_proc.cpus[id])) as f64;
237
238                    if fx.show_cpus_chart.get(&id).is_none() {
239                        let name = format!("CPU #{id}");
240                        let color = {
241                            let hm_color = colors_set.get(&name);
242                            match hm_color {
243                                Some(col) => col.to_color(),
244                                None => {
245                                    if def_colors.len() - 1 < id {
246                                        color!(255, 255, 255)
247                                    } else {
248                                        def_colors[id]
249                                    }
250                                }
251                            }
252                        };
253                        let mut line = LineSeries::new(name, color, fx.show_chart_elements);
254                        line.push(percent);
255
256                        fx.cpu_usage_chart.push_series(line);
257                        fx.show_cpus_chart.insert(id);
258                    } else {
259                        fx.cpu_usage_chart.push_to(id, percent);
260                    }
261                }
262
263                Task::none()
264            }
265            Self::ChangeShowCPUChartElements(elems) => {
266                fx.show_chart_elements = elems;
267
268                fx.cpu_usage_chart.set_max_values(elems);
269                fx.ram_usage_chart.set_max_values(elems);
270
271                Task::none()
272            }
273            Self::CPUFrequencyReceived(state) => {
274                fx.cpu_freq = state;
275                Task::none()
276            }
277            Self::GetCPUFrequency => Task::perform(
278                async move {
279                    let cpu_freq = CpuFreq::new();
280                    match cpu_freq {
281                        Ok(cpu_freq) => DataLoadingState::Loaded(cpu_freq),
282                        Err(why) => DataLoadingState::Error(why.to_string()),
283                    }
284                },
285                |val| Message::DataReceiver(DataReceiverMessage::CPUFrequencyReceived(val)),
286            ),
287            Self::CPUVulnerabilitiesReveived(state) => {
288                fx.cpu_vulnerabilities = state;
289                Task::none()
290            }
291            Self::GetCPUVulnerabilities => Task::perform(
292                async move {
293                    let vuln = Vulnerabilities::new();
294                    match vuln {
295                        Ok(vuln) => DataLoadingState::Loaded(vuln),
296                        Err(why) => DataLoadingState::Error(why.to_string()),
297                    }
298                },
299                |val| Message::DataReceiver(Self::CPUVulnerabilitiesReveived(val)),
300            ),
301            Self::StorageDataReceived(state) => {
302                fx.storages = state;
303                Task::none()
304            }
305            Self::GetStorageData => Task::perform(
306                async move {
307                    let storage = Mounts::new();
308                    match storage {
309                        Ok(storage) => DataLoadingState::Loaded(storage),
310                        Err(why) => DataLoadingState::Error(why.to_string()),
311                    }
312                },
313                |val| Message::DataReceiver(DataReceiverMessage::StorageDataReceived(val)),
314            ),
315            Self::NetworksDataReceived(state) => {
316                fx.networks = state;
317                Task::none()
318            }
319            Self::GetNetworksData => Task::perform(
320                async move {
321                    let net = Networks::new();
322                    match net {
323                        Ok(net) => DataLoadingState::Loaded(net),
324                        Err(why) => DataLoadingState::Error(why.to_string()),
325                    }
326                },
327                |val| Message::DataReceiver(DataReceiverMessage::NetworksDataReceived(val)),
328            ),
329            Self::DMIDataReceived(state) => {
330                if state.some_value() && fx.is_polkit {
331                    fx.dmi_data = state;
332                } else if !fx.is_polkit {
333                    fx.dmi_data = state;
334                }
335                Task::none()
336            }
337            Self::GetDMIData => {
338                if !fx.is_polkit && fx.dmi_data.is_none() && cur_page == Page::DMI {
339                    fx.is_polkit = true;
340                    Task::perform(async move { crate::dmi::get_dmi_data().await }, |val| {
341                        Message::DataReceiver(Self::DMIDataReceived(val))
342                    })
343                } else {
344                    Task::none()
345                }
346            }
347            Self::BatInfoReceived(state) => {
348                fx.bat_data = state;
349                Task::none()
350            }
351            Self::GetBatInfo => Task::perform(
352                async move {
353                    let bat = BatInfo::new();
354                    match bat {
355                        Ok(bat) => DataLoadingState::Loaded(bat),
356                        Err(why) => DataLoadingState::Error(why.to_string()),
357                    }
358                },
359                |val| Message::DataReceiver(Self::BatInfoReceived(val)),
360            ),
361            Self::DRMDataReceived(state) => {
362                fx.drm_data = state;
363                Task::none()
364            }
365            Self::GetDRMData => Task::perform(
366                async move {
367                    let drm = Video::new();
368                    match drm {
369                        Ok(drm) => DataLoadingState::Loaded(drm),
370                        Err(why) => DataLoadingState::Error(why.to_string()),
371                    }
372                },
373                |val| Message::DataReceiver(Self::DRMDataReceived(val)),
374            ),
375            Self::RAMDataReceived(state) => {
376                fx.ram_data = state;
377                Task::none()
378            }
379            Self::GetRAMData => Task::perform(
380                async move {
381                    let ram = RAM::new();
382                    match ram {
383                        Ok(ram) => DataLoadingState::Loaded(ram),
384                        Err(why) => DataLoadingState::Error(why.to_string()),
385                    }
386                },
387                |val| Message::DataReceiver(Self::RAMDataReceived(val)),
388            ),
389            Self::SwapDataReceived(state) => {
390                fx.swap_data = state;
391                Task::none()
392            }
393            Self::GetSwapData => Task::perform(
394                async move {
395                    let swap = Swaps::new();
396                    match swap {
397                        Ok(swap) => DataLoadingState::Loaded(swap),
398                        Err(why) => DataLoadingState::Error(why.to_string()),
399                    }
400                },
401                |val| Message::DataReceiver(Self::SwapDataReceived(val)),
402            ),
403            Self::AddTotalRAMUsage => {
404                let ram = &fx.ram_data;
405                let swap = &fx.swap_data;
406
407                if ram.is_none() {
408                    return Task::none();
409                }
410                let ram = ram.to_option().unwrap();
411                let ram_usage = ram.usage_percentage().unwrap_or(0.);
412
413                let colors_set = &settings.chart_colors.colors;
414                let def_colors = CPU_CHARTS_COLORS;
415                let ram_color = match colors_set.get("RAM") {
416                    Some(col) => col.to_color(),
417                    None => color!(128, 64, 255),
418                };
419
420                if fx.ram_usage_chart.series_count() == 0 {
421                    let mut ram_line = LineSeries::new(
422                        format!("RAM"),
423                        ram_color,
424                        fx.show_chart_elements,
425                    );
426                    ram_line.push(ram_usage);
427                    fx.ram_usage_chart.push_series(ram_line);
428                } else {
429                    fx.ram_usage_chart.push_to(0, ram_usage);
430                }
431
432                if let Some(swap) = swap.to_option() {
433                    let len = swap.swaps.len();
434                    let current_series_cnt = fx.ram_usage_chart.series_count();
435
436                    for id in 0..len {
437                        let series_idx = id + 1;
438                        let swap_usage = swap.swaps[id].usage_percentage().unwrap_or(0.);
439                        let swap_name = swap.swaps[id].filename.clone();
440
441                        if series_idx >= current_series_cnt {
442                            let color = match colors_set.get(&swap_name) {
443                                Some(col) => col.to_color(),
444                                None => {
445                                    if def_colors.len() - 1 < id {
446                                        color!(255, 255, 128)
447                                    } else {
448                                        def_colors[id]
449                                    }
450                                }
451                            };
452                            let mut line = LineSeries::new(
453                                swap.swaps[id].filename.clone(),
454                                color,
455                                fx.show_chart_elements,
456                            );
457                            line.push(swap_usage);
458
459                            fx.ram_usage_chart.push_series(line);
460                        } else {
461                            fx.ram_usage_chart.push_to(series_idx, swap_usage);
462                        }
463                    }
464                }
465                Task::none()
466            }
467            Self::OsReleaseDataReceived(state) => {
468                fx.osrel_data = state;
469                Task::none()
470            }
471            Self::GetOsReleaseData => Task::perform(
472                async move {
473                    let osrel = OsRelease::new();
474                    match osrel {
475                        Ok(osrel) => DataLoadingState::Loaded(osrel),
476                        Err(why) => DataLoadingState::Error(why.to_string()),
477                    }
478                },
479                |val| Message::DataReceiver(Self::OsReleaseDataReceived(val)),
480            ),
481            Self::KernelDataReceived(state) => {
482                fx.kernel_data = state;
483                Task::none()
484            }
485            Self::GetKernelData => Task::perform(
486                async move {
487                    let kern = Kernel::new();
488                    match kern {
489                        Ok(kern) => {
490                            // kern.mods.modules.sort_by_key(|md| md.name.clone());
491                            DataLoadingState::Loaded(kern)
492                        }
493                        Err(why) => DataLoadingState::Error(why.to_string()),
494                    }
495                },
496                |val| Message::DataReceiver(Self::KernelDataReceived(val)),
497            ),
498            Self::KModsDataReceived(state) => {
499                fx.kmods_data = state;
500                Task::none()
501            }
502            Self::GetKModsData => Task::perform(
503                async move {
504                    let kern = KModules::new();
505                    match kern {
506                        Ok(mut kern) => {
507                            kern.modules.sort_by_key(|module| module.name.clone());
508                            DataLoadingState::Loaded(kern)
509                        }
510                        Err(why) => DataLoadingState::Error(why.to_string()),
511                    }
512                },
513                |val| Message::DataReceiver(Self::KModsDataReceived(val)),
514            ),
515            Self::UsersDataReceived(state) => {
516                fx.users_list = state;
517                Task::none()
518            }
519            Self::GetUsersData => Task::perform(
520                async move {
521                    let users = Users::new();
522                    match users {
523                        Ok(mut users) => {
524                            users.users.sort_by_key(|usr| usr.uid);
525                            DataLoadingState::Loaded(users)
526                        }
527                        Err(why) => DataLoadingState::Error(why.to_string()),
528                    }
529                },
530                |val| Message::DataReceiver(Self::UsersDataReceived(val)),
531            ),
532            Self::GroupsDataReceived(state) => {
533                fx.groups_list = state;
534                Task::none()
535            }
536            Self::GetGroupsData => Task::perform(
537                async move {
538                    let groups = Groups::new();
539                    match groups {
540                        Ok(mut groups) => {
541                            groups.groups.sort_by_key(|grp| grp.gid);
542                            DataLoadingState::Loaded(groups)
543                        }
544                        Err(why) => DataLoadingState::Error(why.to_string()),
545                    }
546                },
547                |val| Message::DataReceiver(Self::GroupsDataReceived(val)),
548            ),
549            Self::SystemdServicesReceived((sysd_services, boot_time)) => {
550                fx.sysd_services_list = sysd_services;
551                fx.boot_time = boot_time;
552                Task::none()
553            }
554            Self::GetSystemdServices => Task::perform(
555                async move {
556                    let conn = Connection::session().await;
557                    if let Err(why) = conn {
558                        return (
559                            DataLoadingState::Error(why.to_string()),
560                            DataLoadingState::Loading,
561                        );
562                    }
563                    let conn = conn.unwrap();
564
565                    let srv_list = SystemdServices::new_from_connection(&conn).await;
566                    match srv_list {
567                        Ok(mut srv) => {
568                            if srv.timestamps.total == 0 {
569                                let a = srv.timestamps.calc_boot_time();
570                                if let Err(why) = a {
571                                    return (
572                                        DataLoadingState::Loaded(srv),
573                                        DataLoadingState::Error(why.to_string()),
574                                    );
575                                }
576                            }
577
578                            let boot_time = srv.timestamps;
579                            (
580                                DataLoadingState::Loaded(srv),
581                                DataLoadingState::Loaded(boot_time),
582                            )
583                        }
584                        Err(why) => (
585                            DataLoadingState::Error(why.to_string()),
586                            DataLoadingState::Error("".to_string()),
587                        ),
588                    }
589                },
590                |val| Message::DataReceiver(Self::SystemdServicesReceived(val)),
591            ),
592            Self::SystemDataReceived(state) => {
593                fx.system = state;
594                Task::none()
595            }
596            Self::GetPackagesList => Task::perform(
597                async move {
598                    let pkglist = InstalledPackages::get();
599                    match pkglist {
600                        Ok(pkglist) => DataLoadingState::Loaded(pkglist),
601                        Err(why) => DataLoadingState::Error(why.to_string()),
602                    }
603                },
604                |val| Message::DataReceiver(Self::PackagesListReceived(val)),
605            ),
606            Self::PackagesListReceived(state) => {
607                fx.installed_pkgs_list = state;
608                Task::none()
609            }
610            Self::GetSystemData => Task::perform(
611                async move {
612                    let sys = System::new();
613                    match sys {
614                        Ok(sys) => DataLoadingState::Loaded(sys),
615                        Err(why) => DataLoadingState::Error(why.to_string()),
616                    }
617                },
618                |val| Message::DataReceiver(Self::SystemDataReceived(val)),
619            ),
620        }
621    }
622}
623
624pub type ExportToFilePath = String;
625
626#[derive(Debug, Clone)]
627pub enum ExportManagerMessage {
628    ExportData(ExportToFilePath),
629    ExportFormatSelected(ExportFormat),
630    ExportModeSelected(ExportMode),
631}
632
633impl ExportManagerMessage {
634    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
635        match self {
636            Self::ExportData(path) => fx.export_data(&path),
637            _ => Task::none(),
638        }
639    }
640}
641
642impl Ferrix {
643    fn export_data(&mut self, path: &str) -> Task<Message> {
644        let json = ExportData::from(self)
645            .to_json()
646            .unwrap_or("{error}".to_string());
647        let _ = std::fs::write(path, json);
648        Task::none()
649    }
650}
651
652#[derive(Debug, Clone)]
653pub enum SettingsMessage {
654    ChangeStyle(Style),
655    ChangeUpdatePeriod(u8),
656    ChangeChartsUpdatePeriod(u8),
657    ChangeChartLineThickness(ChartLineThickness),
658    SetChartItemColor(String, (u8, u8, u8)),
659}
660
661impl SettingsMessage {
662    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
663        match self {
664            Self::ChangeStyle(style) => fx.change_style(style),
665            Self::ChangeUpdatePeriod(secs) => fx.change_update_period(secs),
666            Self::ChangeChartsUpdatePeriod(secs) => fx.change_charts_update_period(secs),
667            Self::ChangeChartLineThickness(thick) => fx.change_line_thickness(thick),
668            Self::SetChartItemColor(item, color) => fx.set_chart_item_color(item, color),
669        }
670    }
671}
672
673impl Ferrix {
674    fn change_style(&mut self, style: Style) -> Task<Message> {
675        self.settings.style = style;
676        self.data.cpu_usage_chart.set_style(&style.to_theme());
677        self.data.ram_usage_chart.set_style(&style.to_theme());
678        Task::none()
679    }
680
681    fn change_update_period(&mut self, per: u8) -> Task<Message> {
682        self.settings.update_period = per;
683        Task::none()
684    }
685
686    fn change_charts_update_period(&mut self, per: u8) -> Task<Message> {
687        self.settings.charts_update_period_nsecs = per;
688        Task::none()
689    }
690
691    fn change_line_thickness(&mut self, thick: ChartLineThickness) -> Task<Message> {
692        self.settings.chart_line_thickness = thick;
693        self.data.cpu_usage_chart.set_line_thickness(thick);
694        self.data.ram_usage_chart.set_line_thickness(thick);
695
696        Task::none()
697    }
698
699    fn set_chart_item_color(&mut self, item: String, color: (u8, u8, u8)) -> Task<Message> {
700        self.settings.chart_colors.colors.insert(item, color);
701        self.save_settings()
702    }
703}
704
705#[derive(Debug, Clone)]
706pub enum ButtonsMessage {
707    LinkButtonPressed(String),
708    SaveSettingsButtonPressed,
709    CopyButtonPressed(String),
710
711    ChangeLegendShow(bool),
712    ProcessorSelected(usize),
713}
714
715impl ButtonsMessage {
716    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
717        match self {
718            Self::LinkButtonPressed(url) => fx.go_to_url(&url),
719            Self::SaveSettingsButtonPressed => fx.save_settings(),
720            Self::CopyButtonPressed(s) => iced::clipboard::write(s),
721            Self::ChangeLegendShow(show) => fx.set_show_charts_legend(show),
722            Self::ProcessorSelected(id) => fx.proc_selected(id),
723        }
724    }
725}
726
727impl Ferrix {
728    fn go_to_url(&self, url: &str) -> Task<Message> {
729        // TODO: add error handling
730        let _ = crate::utils::xdg_open(url);
731        Task::none()
732    }
733
734    fn save_settings(&mut self) -> Task<Message> {
735        // TODO: add error handling
736        let _ = self
737            .settings
738            .write(get_home().join(".config").join(SETTINGS_PATH));
739        Task::none()
740    }
741
742    fn set_show_charts_legend(&mut self, show: bool) -> Task<Message> {
743        self.data.cpu_usage_chart.set_show_legend(show);
744        self.data.ram_usage_chart.set_show_legend(show);
745        self.data.show_charts_legend = show;
746        Task::none()
747    }
748
749    fn proc_selected(&mut self, id: usize) -> Task<Message> {
750        self.data.selected_proc = id;
751        Task::none()
752    }
753}
754
755#[derive(Debug, Clone)]
756pub enum KeyboardMessage {
757    Event(Event),
758}
759
760fn get_id(page: Page, m: Modifiers) -> Id {
761    if m.shift() {
762        Id::new(page.scrolled_list_id().unwrap_or(""))
763    } else {
764        Id::new(page.page_id())
765    }
766}
767
768const SCROLL_UP: f32 = -20.;
769const SCROLL_DOWN: f32 = 20.;
770
771fn scroll_up(page: Page, m: Modifiers) -> Task<Message> {
772    let id = get_id(page, m);
773    operation::scroll_by(
774        id,
775        AbsoluteOffset {
776            x: 0.,
777            y: SCROLL_UP,
778        },
779    )
780}
781
782fn scroll_down(page: Page, m: Modifiers) -> Task<Message> {
783    let id = get_id(page, m);
784    operation::scroll_by(
785        id,
786        AbsoluteOffset {
787            x: 0.,
788            y: SCROLL_DOWN,
789        },
790    )
791}
792
793fn scroll_sidebar_up() -> Task<Message> {
794    operation::scroll_by(
795        Id::new("sidebar"),
796        AbsoluteOffset {
797            x: 0.,
798            y: SCROLL_UP,
799        },
800    )
801}
802
803fn scroll_sidebar_down() -> Task<Message> {
804    operation::scroll_by(
805        Id::new("sidebar"),
806        AbsoluteOffset {
807            x: 0.,
808            y: SCROLL_DOWN,
809        },
810    )
811}
812
813fn snap_up(page: Page) -> Task<Message> {
814    let id = Id::new(page.page_id());
815    operation::snap_to(id, RelativeOffset::START)
816}
817
818fn snap_down(page: Page) -> Task<Message> {
819    let id = Id::new(page.page_id());
820    operation::snap_to(id, RelativeOffset::END)
821}
822
823impl KeyboardMessage {
824    pub fn update<'a>(self, fx: &'a mut Ferrix) -> Task<Message> {
825        match self {
826            Self::Event(event) => match event {
827                Event::Keyboard(Kevent::KeyPressed {
828                    key: Key::Named(key::Named::ArrowDown),
829                    modifiers,
830                    ..
831                }) if !modifiers.control() => scroll_down(fx.current_page, modifiers),
832                Event::Keyboard(Kevent::KeyPressed {
833                    key: Key::Named(key::Named::ArrowUp),
834                    modifiers,
835                    ..
836                }) if !modifiers.control() => scroll_up(fx.current_page, modifiers),
837                Event::Keyboard(Kevent::KeyPressed {
838                    key: Key::Named(key::Named::ArrowDown),
839                    modifiers,
840                    ..
841                }) if modifiers.control() => scroll_sidebar_down(),
842                Event::Keyboard(Kevent::KeyPressed {
843                    key: Key::Named(key::Named::ArrowUp),
844                    modifiers,
845                    ..
846                }) if modifiers.control() => scroll_sidebar_up(),
847                Event::Keyboard(Kevent::KeyPressed {
848                    key: Key::Named(key::Named::PageDown),
849                    ..
850                }) => snap_down(fx.current_page),
851                Event::Keyboard(Kevent::KeyPressed {
852                    key: Key::Named(key::Named::PageUp),
853                    ..
854                }) => snap_up(fx.current_page),
855                Event::Keyboard(Kevent::KeyPressed {
856                    key: Key::Named(key::Named::F1),
857                    ..
858                }) => fx.select_page(Page::About),
859                Event::Keyboard(Kevent::KeyPressed {
860                    key: Key::Named(key::Named::F2),
861                    ..
862                }) => fx.select_page(Page::Export),
863                Event::Keyboard(Kevent::KeyPressed {
864                    key: Key::Named(key::Named::F9),
865                    ..
866                }) => fx.select_page(Page::Settings),
867                Event::Keyboard(Kevent::KeyPressed {
868                    key: Key::Named(key::Named::Tab),
869                    modifiers,
870                    ..
871                }) if modifiers.control() => fx.select_page(if modifiers.shift() {
872                    fx.current_page.prev_page()
873                } else {
874                    fx.current_page.next_page()
875                }),
876                _ => Task::none(),
877            },
878        }
879    }
880}